// ==UserScript==
// @name hasa 1.1.5 forschungsmodul komplett - horizon assistant
// @namespace horiversum.hasa
// @version 1.1
// @description Persönlicher Ascension-Assistent für Horizon
// @match https://horiversum.org/game/main/main.php*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @connect techtree.kommandobasis.eu
// ==/UserScript==
/*
==========================================================
HASA – Horizon Ascension Assistant
Version : 1.1
Status : Forschungsmodul – allgemeine Typ- und Iststandsauswertung
Build : 2026-08-25
Änderungen:
- 1.1: Forschungsmodul als allgemeiner Mechanismus abgeschlossen
- STAN-XML ist für bekannte Objekt-IDs die maßgebliche Typquelle
- vor „Unbekannt“ wird immer der lokale STAN-Bestand geprüft
- Logistik und weitere nicht namensbasierte Forschungen werden dadurch korrekt eingeordnet
- Forschungs-Iststand wird vollständig aus der sichtbaren Horizon-Forschungsseite neu aufgebaut
- nach Forschungsabschluss/F5/erneutem Öffnen wird der Soll/Ist-Vergleich ohne Sonderfall aktualisiert
- Ascension-Reset aus 1.0.3l bleibt erhalten
- HASA passt seine Breite an den verfügbaren rechten Bildschirmbereich an
- auf kleinen Displays wird HASA horizontal scrollbar statt Horizon zu überdecken
- 1.0.3l: Horizon-Seitenstand ist je Bereich autoritativ; alte Forschungs-/Gebäudewerte werden vor dem Scan entfernt
- damit bleiben Werte aus dem Leben vor einer Ascension nicht als erledigt gespeichert
- SOLARENERGYTECH / Solarenergietechnik wird verbindlich als Forschung eingeordnet
- J bleibt J: UI-Korrektur in 1.0.3j integriert
- Astrogation und Terraforming werden sicher als Forschung eingeordnet
- Forschungsplan-Liste wieder ein-/ausklappbar
- Forschungsseite wird bei jedem HASA-Seitenscan vollständig neu als Iststand eingelesen
- zusätzlicher Horizon-Fallback liest Zeilen wie „Physik 16 (physics - Forschungsstufe 16)“
- technische Horizon-ID wird dabei mitgespeichert (z. B. PHYSICS, PSYCHOLOGY, REFINERYTECH)
- F5 bzw. erneutes Öffnen/Ändern der Forschungsseite bewertet den Plan danach sofort neu
- Forschungsplaner neu begonnen: keine rekursive Sammlung in dieser Teststufe
- nur die direkten Mindestanforderungen des ausgewählten Ziels werden gelesen
- direkte Anforderungen werden sofort gegen den lokalen Horizon-Iststand geprüft
- erfüllte Anforderungen verschwinden in der reduzierten Ansicht
- keine Untervoraussetzungen, Reverse-Pfade oder Maximalstufen in dieser Teststufe
- erfüllte Voraussetzungen beenden ihren Zweig sofort; keine Rekursion unter bereits vorhandene Gebäude/Forschungen
- bereits erfüllte Zweige werden beim TechTree-Import nicht mehr nachgeladen
- Namen mit der Endung „Technik“ werden verbindlich als Forschung eingeordnet
- sichtbarer Debugbereich bleibt erhalten
- STAN-Kategorien „Schwarm“ und „Arkane“ im Ziel-Explorer ausblenden
- Fremdrassen-Daten bleiben im lokalen STAN-Bestand erhalten und können später für Kampfanalyse genutzt werden
- Forschungs- und Gebäudestände weiterhin nur von der sichtbar geöffneten Horizon-Seite lesen
- nach dem Öffnen eines Horizon-Unterbereichs automatisch erneut einlesen
- Forschungsplanung danach ohne weiteren HASA-Klick aktualisieren
- keine Horizon-Seiten im Hintergrund abrufen
- Debugprotokoll für Auswahl und Sammlung der TechTree-Anforderungen
- protokolliert Übernahme, Ablehnung, Zusammenführung und Rekursionssprünge
Idee & Konzept:
Styl
Programmierung:
ChatGPT
Grundsatz:
HASA informiert, analysiert und erinnert.
HASA spielt nicht.
Speicherung:
Alle Einstellungen werden lokal im Tampermonkey-Speicher
(GM_getValue / GM_setValue) gespeichert.
Fallback: Browser-localStorage, falls GM-Speicher nicht verfügbar ist.
==========================================================
*/
(function () {
'use strict';
// Fremdrassen: nicht als erreichbare Spielerziele im Explorer anzeigen.
// Die Datensätze bleiben bewusst gespeichert, damit sie später z. B.
// für einen Kampfsimulator oder Flottenvergleich genutzt werden können.
const STAN_EXPLORER_AUSGEBLENDETE_KATEGORIEN = ['schwarm', 'arkane'];
function istStanExplorerKategorieAusgeblendet(kategorie) {
const normalisiert = normalisiereTechTreeText(kategorie).toLowerCase();
return STAN_EXPLORER_AUSGEBLENDETE_KATEGORIEN.includes(normalisiert);
}
const HASA_SICHERE_FORSCHUNGS_IDS = new Set([
'ASTROGATION', 'TERRAFORMINGTECH', 'TERRAFORMING',
'PHYSICS', 'PSYCHOLOGY', 'NUCLEARPHYSICS', 'SOLARENERGYTECH'
]);
const HASA_VERSION = '1.1 – Forschungsmodul komplett · poliert';
const HASA_BUILD_LABEL = '1.1-poliert-stufenbereiche-2026-08-27';
/*
* hasa-Positionswerte ab 0.7 Beta
*
* Die Position der hasa-Box hängt NICHT mehr vom Text „Ascension“ ab.
* Ascension entscheidet nur noch über den INHALT der Box.
*
* Grundlage ist der feste Horizon-Spielbereich:
* - linke Navigation: 128px
* - nutzbarer linker Spielbereich: ca. 642px
* - daraus ergibt sich als Standard-Anker: 128 + 642 = 770px
*
* Falls Horizon in einem Skin/Zoom minimal abweicht, sind nur diese
* Werte anzupassen. Keine Pixelwerte mehr tief in der Funktion.
*/
const HASA_POSITION = {
SPIEL_LINKS_X: 128,
SPIEL_BREITE_X: 642,
BOX_X_OFFSET: 0,
BOX_OBEN_Y: 132,
BOX_Y_OFFSET: 0,
RAND_RECHTS_MIN: 12,
// 0.8.2 Beta:
// Optischer Standardanker:
// oben = Oberkante der Buddies-Leiste
// links = linke Kante des Nachrichten-/Chatfensters unten
// Wenn einer der Anker nicht gefunden wird, greift der alte Fallback.
BUDDIES_Y_FALLBACK: 68,
CHAT_LINKS_FALLBACK: 286,
ANKER_Y_OFFSET: 0,
ANKER_X_OFFSET: 0,
// 0.8.3 Beta:
// Anker nur übernehmen, wenn sie plausibel zum großen Horizon-Layout passen.
// So vermeiden wir, dass HASA an einer kleinen Mini-Box oben links kleben bleibt.
BUDDIES_MIN_LINKS: 350,
BUDDIES_MAX_OBEN: 160,
CHAT_MIN_OBEN_ANTEIL: 0.45,
CHAT_MIN_LINKS: 120,
CHAT_MIN_BREITE: 500,
// 0.9.6 Beta: Magnetisches Einrasten beim Verschieben.
SNAP_ABSTAND: 18
};
const STORAGE_FAVORITEN = 'haa_favoriten_v05';
const STORAGE_SPALTEN = 'haa_spalten_v05';
const STORAGE_ZU = 'haa_zu_v05';
const STORAGE_BAUALARM = 'haa_baualarm_v055';
const STORAGE_BAUSTATUS = 'haa_baustatus_v060';
const STORAGE_ALARM_AUSGELOEST = 'haa_alarm_ausgeloest_v060';
const STORAGE_FENSTER_POSITION = 'haa_fenster_position_v090';
const STORAGE_ASCENSION_STATUS = 'haa_ascension_status_v091';
const STORAGE_ZIEL = 'haa_ziel_v095';
const STORAGE_VORLAUF = 'haa_vorlauf_v095';
const STORAGE_VORALARM_AUSGELOEST = 'haa_voralarm_ausgeloest_v095';
const STORAGE_BAUUEBERWACHUNG = 'haa_bauueberwachung_v099';
const STORAGE_FORSCHUNGSUEBERWACHUNG = 'haa_forschungsueberwachung_v101b';
const STORAGE_TECHTREE_GRAPH = 'haa_techtree_graph_v103i';
const STORAGE_TECHTREE_STATUS = 'haa_techtree_status_v103f';
const STORAGE_TECHTREE_DEBUG = 'haa_techtree_debug_v103g';
const STORAGE_UI_BOX_ZU = 'haa_ui_box_zu_v101e1';
const STORAGE_UI_AKTIVER_BEREICH = 'haa_ui_aktiver_bereich_v101e2';
const STORAGE_UI_SCROLL = 'haa_ui_scroll_v101e2';
const STORAGE_TECHTREE_SPIELSTAND = 'haa_techtree_spielstand_v102';
const STORAGE_TECHTREE_ERLEDIGT_ZEIGEN = 'haa_techtree_erledigt_zeigen_v102';
const STORAGE_STAN_STAMMDATEN = 'haa_stan_stammdaten_v103';
const STORAGE_STAN_VERSIONEN = 'haa_stan_versionen_v103a';
const STORAGE_STAN_STATUS = 'haa_stan_status_v103';
const STORAGE_STAN_AUSWAHL = 'haa_stan_auswahl_v103';
const STORAGE_STAN_NAVIGATION = 'haa_stan_navigation_v103';
const STORAGE_STAN_AUSWAHL_OFFEN = 'haa_stan_auswahl_offen_v103b';
const STORAGE_SEITENSCAN_STATUS = 'haa_seitenscan_status_v103';
const TECHTREE_BASIS_URL = 'https://techtree.kommandobasis.eu/objects/details/id/';
const STAN_START_DATENSTAND = '20260802';
const STAN_RHYTHMUS_TAGE = 7;
const TECHTREE_CACHE_MS = 24 * 60 * 60 * 1000;
const HASA_ALARM_VORLAUF_SEKUNDEN = 2;
const HASA_SYNC_INTERVALL_MS = 60000;
const STANDARD_FAVORITEN = [
'Erhöht Gebäudeausbaugeschwindigkeit',
'Reduziert Gebäudepreise',
'Ascension-Vervielfältigung'
];
const STANDARD_SPALTEN = {
stufe: true,
letzte: true,
kosten: true,
invest: true
};
function verdopplung(max) {
const a = [];
let k = 1;
for (let i = 0; i < max; i++) {
a.push(k);
k *= 2;
}
return a;
}
function skill(kategorie, kosten, max) {
return { kategorie, kosten, max };
}
const K31 = verdopplung(31);
const HASA_SKILLS = {
// Produktion
'Erhöht Gebäudeausbaugeschwindigkeit': skill('Produktion', K31, 31),
'Erhöht Gebäudebauplätze': skill('Produktion', verdopplung(10), 10),
'Erhöht Gewinn von Bauplatz durch Infrastruktur': skill('Produktion', K31, 31),
'Erhöht die Größe des Mondbauplatzes': skill('Produktion', K31, 31),
'Erhöht Schiffsbaugeschwindigkeit': skill('Produktion', K31, 31),
'Erhöht Schiffsreparaturgeschwindigkeit': skill('Produktion', K31, 31),
'Erhöht Waffenbaugeschwindigkeit': skill('Produktion', K31, 31),
// Rohstoffe
'Erhöhte Metallproduktion': skill('Rohstoffe', K31, 31),
'Erhöhte Mineralproduktion': skill('Rohstoffe', K31, 31),
'Erhöhte Treibstoffproduktion': skill('Rohstoffe', K31, 31),
'Vergrößerte Metalllager': skill('Rohstoffe', K31, 31),
'Vergrößerte Minerallager': skill('Rohstoffe', K31, 31),
'Vergrößerte Treibstofflager': skill('Rohstoffe', K31, 31),
'Erhöhte Bevölkerung': skill('Rohstoffe', K31, 31),
'Erhöhte Energieproduktion': skill('Rohstoffe', K31, 31),
'Reduziert Gebäudepreise': skill('Rohstoffe', K31, 31),
'Reduziert Forschungskosten': skill('Rohstoffe', K31, 31),
// Flotte
'Senkt Treibstoffverbrauch der Schiffe': skill('Flotte', K31, 31),
'Erhöht die interplanetare Geschwindigkeit der Schiffe': skill('Flotte', K31, 31),
'Erhöht die Flottenkontrolle der Flaggschiffe': skill('Flotte', K31, 31),
'Erhöht Rohstoffabbau durch Bergbauschiffe': skill('Flotte', K31, 31),
'Erhöht den Laderaume der Schiffe': skill('Flotte', K31, 31),
'Reduziert den Bedarf an Hyperraumkontrollzyklen der Schiffe': skill('Flotte', K31, 31),
'Erhöht die Größe des Raumhafens': skill('Flotte', K31, 31),
'Erhöht die Größe der Raumstation': skill('Flotte', K31, 31),
'Erhöht die Größe des Flottenbunkers': skill('Flotte', K31, 31),
// Gebäude
'Grundausstattung des Heimatplaneten': skill('Gebäude', [1, 2, 4], 3),
'Grundausstattung der Kolonien': skill('Gebäude', [1, 2], 2),
// Imperium
'Imperium Coloniale Supremum': skill('Imperium', [10, 100, 1000], 3),
'Erhöhte Koloniekontrolle': skill('Imperium', [4, 16, 64, 256, 1024], 5),
// 'Ascension-Vervielfältigung': skill('Imperium', [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192], 31),
'Ascension-Vervielfältigung': skill('Imperium', K31, 31),
'Panzerbrechende Maschinenkanonen': skill('Waffen', [1, 10, 100], 3),
'Aktivkühlung von Maschinenkanonen': skill('Waffen', [1, 10, 100], 3),
'Stickstoffkühlung von Protonenkanonen': skill('Waffen', [1, 10, 100], 3),
'Magnetische Plasmaverdichtung': skill('Waffen', [1, 10, 100], 3),
'Fokussierte Plasmafeldstabilisierung': skill('Waffen', [1, 10, 100], 3),
'Resonanzmodulation von Ionenstrahlen': skill('Waffen', [1, 10, 100], 3),
'Hochenergetische Ionisierung': skill('Waffen', [1, 10, 100], 3),
// Perks
'Alle Gebäude fertig stellen': skill('Perks', [1], 1),
'Aktuelle Forschung fertig stellen': skill('Perks', [1], 1),
'Alle Schiffe fertig stellen': skill('Perks', [1], 1),
'Alle Verteidigungsanlagen fertig stellen': skill('Perks', [1], 1),
'Alle Expeditionen abschließen': skill('Perks', [1], 1),
'Alle Planetaren Hypersprünge abschließen': skill('Perks', [1], 1),
'Alle Flotten kehren sofort zurück': skill('Perks', [1], 1),
'Forschungssprung': skill('Perks', [25], 1),
'Aggregatsverbesserung': skill('Perks', [25], 1),
// Ereignisse
'Reduziert Risiko von Alien Flotten': skill('Ereignisse', K31, 31),
'Erhöht die Häufigkeit von Motivationsvideos': skill('Ereignisse', K31, 31),
// Forschung
'Grundausstattung an Forschungen': skill('Forschung', [10, 20, 40, 80, 160], 5)
};
function text(el) {
return el ? el.textContent.replace(/\s+/g, ' ').trim() : '';
}
function zahl(s) {
if (!s || s === 'N/A') return null;
s = String(s).replace(',', '.').replace(/[^\d.]/g, '');
return s ? Number(s) : null;
}
function esc(s) {
return String(s ?? '').replace(/[&<>"']/g, c => ({
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
}[c]));
}
function summeBis(kosten, stufe) {
if (!kosten || stufe <= 0) return 0;
let s = 0;
for (let i = 0; i < stufe && i < kosten.length; i++) {
s += kosten[i];
}
return s;
}
function letzteAusDatenbank(name, stufe) {
const db = HASA_SKILLS[name];
if (!db || stufe <= 0) return null;
return db.kosten[stufe - 1] ?? null;
}
function investAusDatenbank(name, stufe) {
const db = HASA_SKILLS[name];
if (!db) return null;
return summeBis(db.kosten, stufe);
}
function leseAP() {
for (const el of Array.from(document.querySelectorAll('p, div'))) {
const m = text(el).match(/Du kannst\s+([\d.,]+)\s+Ascend-Punkte ausgeben/i);
if (m) return zahl(m[1]);
}
return null;
}
function leseAPBeimNaechstenReset() {
const seitenText = text(document.body);
const muster = [
/Beim Zurücksetzen werden\s+([\d.,]+)\s+Punkte verfügbar/i,
/Beim Zuruecksetzen werden\s+([\d.,]+)\s+Punkte verfuegbar/i,
/(?:nächsten|naechsten)\s+(?:Reset|Zurücksetzen)[^\d]{0,80}([\d.,]+)\s+(?:Ascend-?)?Punkte/i
];
for (const regex of muster) {
const m = seitenText.match(regex);
if (m) return zahl(m[1]);
}
return null;
}
function leseAscensions() {
const daten = [];
const kats = Array.from(document.querySelectorAll('.AscAccCatHeadBox'));
kats.forEach(catBox => {
const kategorie = text(catBox.querySelector('.AscAccCatHead'));
const content = catBox.nextElementSibling;
if (!kategorie || !content) return;
const rows = Array.from(content.querySelectorAll('tr.AscAccBldClsBody'));
for (let i = 0; i < rows.length; i += 2) {
const nameEl = rows[i].querySelector('.AscAccCategory');
const row2 = rows[i + 1];
if (!nameEl || !row2) continue;
const werte = Array.from(row2.querySelectorAll('.AscAccFuncDescTextright')).map(text);
const stufe = zahl(werte[0]);
const kostenText = werte[1] || '';
const kosten = zahl(kostenText);
let name = text(nameEl);
if (stufe !== null) {
name = name.replace(new RegExp('\\s+' + stufe + '$'), '');
}
daten.push({
kategorie,
name,
stufe,
kosten,
kostenText,
letzte: letzteAusDatenbank(name, stufe),
invest: investAusDatenbank(name, stufe),
meta: HASA_SKILLS[name] || null
});
}
});
return daten;
}
function ladeFavoriten(daten) {
const gespeicherteFavoriten = laden(STORAGE_FAVORITEN, null);
if (Array.isArray(gespeicherteFavoriten)) {
return gespeicherteFavoriten;
}
return daten.filter(e => STANDARD_FAVORITEN.includes(e.name)).map(e => e.name);
}
function ladeSpalten() {
const gespeicherteSpalten = laden(STORAGE_SPALTEN, null);
if (gespeicherteSpalten && typeof gespeicherteSpalten === 'object' && !Array.isArray(gespeicherteSpalten)) {
return Object.assign({}, STANDARD_SPALTEN, gespeicherteSpalten);
}
return Object.assign({}, STANDARD_SPALTEN);
}
function gmSpeicherVerfuegbar() {
return typeof GM_getValue === 'function' && typeof GM_setValue === 'function';
}
function hasaSpeicherName() {
return gmSpeicherVerfuegbar() ? 'Tampermonkey GM' : 'localStorage';
}
function baueHasaStatusHtml() {
let html = '';
html += '
';
html += '
Version: ' + esc(HASA_VERSION) + '
';
html += '
Speicher: ' + esc(hasaSpeicherName()) + '
';
html += '
Build: ' + esc(HASA_BUILD_LABEL) + '
';
html += '
';
return html;
}
function ladeHasaZiel() {
const ziel = laden(STORAGE_ZIEL, null);
if (ziel && typeof ziel === 'object' && !Array.isArray(ziel)) {
return Object.assign({ typ: 'aktueller-bau', name: 'Aktueller Bau' }, ziel);
}
return { typ: 'aktueller-bau', name: 'Aktueller Bau' };
}
function speichereHasaZiel(typ) {
speichern(STORAGE_ZIEL, {
typ: typ || 'aktueller-bau',
name: typ === 'cc' ? 'Koloniezentrum / Kolonialverwaltung' : 'Aktueller Bau',
gespeichertAm: Date.now()
});
}
function ladeVorlaufSekunden() {
const wert = Number(laden(STORAGE_VORLAUF, 60));
if (!Number.isFinite(wert) || wert < 0) {
return 60;
}
return wert;
}
function speichereVorlaufSekunden(wert) {
const sekunden = Number(wert);
speichern(STORAGE_VORLAUF, Number.isFinite(sekunden) && sekunden >= 0 ? sekunden : 60);
}
function baueVorlaufOption(wert, label, aktuell) {
return '';
}
function baueZielUndVorlaufHtml() {
const ziel = ladeHasaZiel();
const vorlauf = ladeVorlaufSekunden();
let html = '';
html += '';
html += '';
html += '';
html += '';
html += '';
html += '
';
return html;
}
function verbindeZielUndVorlaufButtons(neuAufbauen) {
const zielSelect = document.getElementById('haa-ziel-select');
if (zielSelect) {
zielSelect.onchange = function () {
speichereHasaZiel(zielSelect.value);
if (typeof neuAufbauen === 'function') {
neuAufbauen();
}
};
}
const vorlaufSelect = document.getElementById('haa-vorlauf-select');
if (vorlaufSelect) {
vorlaufSelect.onchange = function () {
speichereVorlaufSekunden(vorlaufSelect.value);
speichern(STORAGE_VORALARM_AUSGELOEST, '');
HAA_VORALARM_AUSGELOEST_LAUF = '';
if (typeof neuAufbauen === 'function') {
neuAufbauen();
}
};
}
}
function speichern(key, wert) {
try {
if (gmSpeicherVerfuegbar()) {
GM_setValue(key, wert);
return;
}
} catch (e) {
console.warn('HASA GM-Speicher fehlgeschlagen, nutze localStorage:', e);
}
localStorage.setItem(key, JSON.stringify(wert));
}
function laden(key, fallback) {
try {
if (gmSpeicherVerfuegbar()) {
const wert = GM_getValue(key, undefined);
if (wert !== undefined && wert !== null && wert !== '') {
return wert;
}
}
} catch (e) {
console.warn('HASA GM-Lesen fehlgeschlagen, nutze localStorage:', e);
}
try {
const s = localStorage.getItem(key);
if (s === null || s === undefined || s === '') {
return fallback;
}
return JSON.parse(s);
} catch (e) {
return fallback;
}
}
function istZu() {
return laden(STORAGE_ZU, '0') === '1';
}
function setZu(zu) {
speichern(STORAGE_ZU, zu ? '1' : '0');
}
function px(wert) {
return Math.round(Number(wert)) + 'px';
}
function findeHorizonSpielLayout() {
const divs = Array.from(document.querySelectorAll('div'));
return divs.find(function (el) {
const style = el.getAttribute('style') || '';
return /left:\s*128px/i.test(style)
&& /top:\s*0px/i.test(style)
&& /right:\s*0px/i.test(style);
}) || null;
}
function rechteckGueltig(r) {
return r
&& Number.isFinite(r.left)
&& Number.isFinite(r.top)
&& Number.isFinite(r.width)
&& Number.isFinite(r.height)
&& r.width > 0
&& r.height > 0;
}
function findeBuddiesAnker() {
const elemente = Array.from(document.querySelectorAll('a, div, td, span, nobr, font'));
const kandidaten = [];
for (const el of elemente) {
if (!/Buddies\s*:/i.test(text(el))) {
continue;
}
let kandidat = el;
for (let i = 0; i < 5 && kandidat; i++) {
const r = kandidat.getBoundingClientRect();
if (rechteckGueltig(r)
&& r.width >= 120
&& r.height >= 18
&& r.left >= HASA_POSITION.BUDDIES_MIN_LINKS
&& r.top >= 0
&& r.top <= HASA_POSITION.BUDDIES_MAX_OBEN) {
kandidaten.push({ el: kandidat, rect: r });
}
kandidat = kandidat.parentElement;
}
}
if (!kandidaten.length) {
return null;
}
kandidaten.sort(function (a, b) {
// Oberste plausible Buddies-Leiste im rechten Kopfbereich.
return a.rect.top - b.rect.top;
});
return kandidaten[0].el;
}
function findeNachrichtenFensterAnker() {
const elemente = Array.from(document.querySelectorAll('div, table, tr, td, input, textarea, button'));
const kandidaten = [];
const minTop = window.innerHeight * HASA_POSITION.CHAT_MIN_OBEN_ANTEIL;
for (const el of elemente) {
const t = text(el);
if (!t) {
continue;
}
const istChat = /An\s+Allianz\s+senden/i.test(t)
|| /\[\d{2}:\d{2}\]/.test(t);
if (!istChat) {
continue;
}
let kandidat = el;
for (let i = 0; i < 7 && kandidat; i++) {
const r = kandidat.getBoundingClientRect();
if (rechteckGueltig(r)
&& r.width >= HASA_POSITION.CHAT_MIN_BREITE
&& r.left >= HASA_POSITION.CHAT_MIN_LINKS
&& r.top >= minTop) {
kandidaten.push({ el: kandidat, rect: r });
}
kandidat = kandidat.parentElement;
}
}
if (!kandidaten.length) {
return null;
}
kandidaten.sort(function (a, b) {
// Möglichst das große, untere Nachrichtenfenster nehmen,
// aber keine Mini-Boxen aus dem Kopfbereich.
if (Math.abs(b.rect.top - a.rect.top) > 8) {
return b.rect.top - a.rect.top;
}
return b.rect.width - a.rect.width;
});
return kandidaten[0].el;
}
function ermittleSpielLinks() {
const layout = findeHorizonSpielLayout();
if (layout) {
const r = layout.getBoundingClientRect();
if (Number.isFinite(r.left) && r.left > 0) {
return r.left;
}
}
return HASA_POSITION.SPIEL_LINKS_X;
}
function ermittleBoxLinks(box) {
const chat = findeNachrichtenFensterAnker();
if (chat) {
const r = chat.getBoundingClientRect();
if (rechteckGueltig(r)) {
return r.left + HASA_POSITION.ANKER_X_OFFSET;
}
}
// Fallback, wenn die Chat-/Nachrichtenbox im aktuellen Fenster nicht sichtbar ist.
if (HASA_POSITION.CHAT_LINKS_FALLBACK !== null && HASA_POSITION.CHAT_LINKS_FALLBACK !== undefined) {
return HASA_POSITION.CHAT_LINKS_FALLBACK + HASA_POSITION.ANKER_X_OFFSET;
}
const basisLinks = ermittleSpielLinks();
const links = basisLinks + HASA_POSITION.SPIEL_BREITE_X + HASA_POSITION.BOX_X_OFFSET;
if (!box || !box.offsetWidth) {
return links;
}
const maxLinks = window.innerWidth - box.offsetWidth - HASA_POSITION.RAND_RECHTS_MIN;
if (Number.isFinite(maxLinks) && maxLinks > 0) {
return Math.min(links, maxLinks);
}
return links;
}
function ermittleBoxOben() {
const buddies = findeBuddiesAnker();
if (buddies) {
const r = buddies.getBoundingClientRect();
if (rechteckGueltig(r)) {
return r.top + HASA_POSITION.ANKER_Y_OFFSET;
}
}
if (HASA_POSITION.BUDDIES_Y_FALLBACK !== null && HASA_POSITION.BUDDIES_Y_FALLBACK !== undefined) {
return HASA_POSITION.BUDDIES_Y_FALLBACK + HASA_POSITION.ANKER_Y_OFFSET;
}
return HASA_POSITION.BOX_OBEN_Y + HASA_POSITION.BOX_Y_OFFSET;
}
function setzeBoxPosition(box, links, oben) {
box.style.left = px(links);
box.style.top = px(oben);
}
function ladeGespeicherteFensterPosition() {
const pos = laden(STORAGE_FENSTER_POSITION, null);
if (!pos) {
return null;
}
const links = Number(pos.left);
const oben = Number(pos.top);
if (!Number.isFinite(links) || !Number.isFinite(oben)) {
return null;
}
return { left: links, top: oben };
}
function speichereFensterPosition(box) {
if (!box) {
return;
}
const r = box.getBoundingClientRect();
speichern(STORAGE_FENSTER_POSITION, {
left: Math.round(r.left),
top: Math.round(r.top),
gespeichertAm: Date.now()
});
}
function begrenzeFensterPosition(box, links, oben) {
const rand = 8;
const breite = box && box.offsetWidth ? box.offsetWidth : 380;
const hoehe = box && box.offsetHeight ? box.offsetHeight : 120;
const maxLinks = Math.max(rand, window.innerWidth - breite - rand);
const maxOben = Math.max(rand, window.innerHeight - hoehe - rand);
return {
left: Math.min(Math.max(rand, links), maxLinks),
top: Math.min(Math.max(rand, oben), maxOben)
};
}
function snappeFensterPosition(box, links, oben, eventObj) {
if (eventObj && eventObj.shiftKey) {
return { left: links, top: oben };
}
const abstand = HASA_POSITION.SNAP_ABSTAND || 18;
const rand = 8;
const breite = box && box.offsetWidth ? box.offsetWidth : 380;
const hoehe = box && box.offsetHeight ? box.offsetHeight : 120;
let neuLinks = links;
let neuOben = oben;
const rechterRand = window.innerWidth - breite - rand;
const untererRand = window.innerHeight - hoehe - rand;
if (Math.abs(neuLinks - rand) <= abstand) {
neuLinks = rand;
}
if (Math.abs(neuLinks - rechterRand) <= abstand) {
neuLinks = rechterRand;
}
if (Math.abs(neuOben - rand) <= abstand) {
neuOben = rand;
}
if (Math.abs(neuOben - untererRand) <= abstand) {
neuOben = untererRand;
}
const buddies = findeBuddiesAnker();
if (buddies) {
const r = buddies.getBoundingClientRect();
if (rechteckGueltig(r) && Math.abs(neuOben - r.top) <= abstand) {
neuOben = r.top;
}
}
const chat = findeNachrichtenFensterAnker();
if (chat) {
const r = chat.getBoundingClientRect();
if (rechteckGueltig(r)) {
if (Math.abs(neuLinks - r.left) <= abstand) {
neuLinks = r.left;
}
if (Math.abs(neuOben - r.top) <= abstand) {
neuOben = r.top;
}
}
}
return { left: neuLinks, top: neuOben };
}
function positioniereBox(box) {
const gespeichert = ladeGespeicherteFensterPosition();
if (gespeichert) {
const begrenzt = begrenzeFensterPosition(box, gespeichert.left, gespeichert.top);
setzeBoxPosition(box, begrenzt.left, begrenzt.top);
return;
}
setzeBoxPosition(
box,
ermittleBoxLinks(box),
ermittleBoxOben()
);
}
function istDragSperrElement(el) {
return !!(el && el.closest && el.closest('button, input, textarea, select, option, a, label'));
}
function starteHasaDrag(e) {
if (e.button !== 0 || istDragSperrElement(e.target)) {
return;
}
const box = document.getElementById('haa-box');
if (!box) {
return;
}
const r = box.getBoundingClientRect();
HAA_DRAG_AKTIV = true;
HAA_DRAG_BOX = box;
HAA_DRAG_START_X = e.clientX;
HAA_DRAG_START_Y = e.clientY;
HAA_DRAG_BOX_X = r.left;
HAA_DRAG_BOX_Y = r.top;
box.style.cursor = 'grabbing';
box.style.userSelect = 'none';
e.preventDefault();
}
function bewegeHasaDrag(e) {
if (!HAA_DRAG_AKTIV || !HAA_DRAG_BOX) {
return;
}
const links = HAA_DRAG_BOX_X + (e.clientX - HAA_DRAG_START_X);
const oben = HAA_DRAG_BOX_Y + (e.clientY - HAA_DRAG_START_Y);
const geschnappt = snappeFensterPosition(HAA_DRAG_BOX, links, oben, e);
const begrenzt = begrenzeFensterPosition(HAA_DRAG_BOX, geschnappt.left, geschnappt.top);
setzeBoxPosition(HAA_DRAG_BOX, begrenzt.left, begrenzt.top);
e.preventDefault();
}
function beendeHasaDrag() {
if (!HAA_DRAG_AKTIV || !HAA_DRAG_BOX) {
return;
}
HAA_DRAG_BOX.style.cursor = 'move';
HAA_DRAG_BOX.style.userSelect = '';
speichereFensterPosition(HAA_DRAG_BOX);
HAA_DRAG_AKTIV = false;
HAA_DRAG_BOX = null;
}
function aktiviereHasaDrag(box) {
if (!box || box.dataset.hasaDrag === '1') {
return;
}
box.dataset.hasaDrag = '1';
box.title = 'hasa-Fenster mit der Maus verschieben';
box.style.cursor = 'move';
box.addEventListener('mousedown', starteHasaDrag);
}
document.addEventListener('mousemove', bewegeHasaDrag);
document.addEventListener('mouseup', beendeHasaDrag);
function grundBox() {
const alt = document.getElementById('haa-box');
if (alt) alt.remove();
const box = document.createElement('div');
box.id = 'haa-box';
box.style.position = 'fixed';
box.style.zIndex = '999999';
box.style.background = '#211d18';
box.style.color = '#ffffff';
box.style.border = '2px solid #b89b5e';
box.style.padding = '8px';
box.style.fontFamily = 'Arial, sans-serif';
box.style.fontSize = '13px';
// HASA 1.1: kleine Displays behalten die normale Schriftgröße.
// Bei Platzmangel wird innerhalb von HASA horizontal gescrollt.
box.style.minWidth = '0';
box.style.width = '760px';
box.style.maxWidth = 'calc(100vw - 24px)';
box.style.maxHeight = 'calc(100vh - 16px)';
box.style.overflowY = 'hidden';
box.style.overflowX = 'auto';
box.style.boxShadow = '0 0 8px #000';
box.style.borderRadius = '3px';
document.body.appendChild(box);
const basisLinks = ermittleBoxLinks(null);
const verfuegbar = Math.max(220, window.innerWidth - basisLinks - HASA_POSITION.RAND_RECHTS_MIN);
box.style.width = Math.min(760, verfuegbar) + 'px';
box.style.maxWidth = verfuegbar + 'px';
positioniereBox(box);
aktiviereHasaDrag(box);
return box;
}
function baueBauzeitAnalyse(daten) {
const speed = daten.find(e => e.name === 'Erhöht Gebäudeausbaugeschwindigkeit');
const preis = daten.find(e => e.name === 'Reduziert Gebäudepreise');
if (!speed || !preis) return '';
const stSpeed = speed.stufe || 0;
const stPreis = preis.stufe || 0;
const speedFaktor = Math.pow(1.30, stSpeed);
const kostenFaktor = Math.pow(0.80, stPreis);
const bauzeitFaktor = kostenFaktor / speedFaktor;
const prozent = bauzeitFaktor * 100;
const ersparnis = 100 - prozent;
return ''
+ ''
+ '
🏗 Bauzeit
'
+ '
Speed St. ' + esc(stSpeed) + ' → ×' + speedFaktor.toFixed(3) + '
'
+ '
Preis St. ' + esc(stPreis) + ' → ×' + kostenFaktor.toFixed(3) + '
'
+ '
' + prozent.toFixed(1) + '% der Grundbauzeit
'
+ '
verkürzt um ca. ' + ersparnis.toFixed(1) + '%
'
+ '
';
}
function berechneInvestGesamt(daten) {
let summe = 0;
let unbekannt = 0;
daten.forEach(e => {
if (typeof e.invest === 'number') {
summe += e.invest;
} else if ((e.stufe || 0) > 0) {
unbekannt++;
}
});
return { summe, unbekannt };
}
function formatZahl(n) {
if (n === null || n === undefined || Number.isNaN(n)) return '?';
return Number(n).toLocaleString('de-DE', { maximumFractionDigits: 2 });
}
function erstelleAscensionStatus(daten, ap, apNaechsterReset) {
const inv = berechneInvestGesamt(daten);
return {
apFrei: ap,
apNaechsterReset: apNaechsterReset,
apInvestiert: inv.summe,
apGesamt: ap !== null ? inv.summe + ap : null,
unbekannt: inv.unbekannt,
skills: Array.isArray(daten) ? daten.map(function (e) {
return {
kategorie: e.kategorie,
name: e.name,
stufe: e.stufe,
kosten: e.kosten,
kostenText: e.kostenText,
letzte: e.letzte,
invest: e.invest
};
}) : [],
zuletztGesehen: Date.now()
};
}
function speichereAscensionStatus(status) {
if (!status) {
return;
}
speichern(STORAGE_ASCENSION_STATUS, status);
}
function ladeAscensionStatus() {
return laden(STORAGE_ASCENSION_STATUS, null);
}
function baueAscensionDashboardHtml(status) {
let html = '';
html += '';
html += '
';
html += '
⭐ Ascension
';
html += '
➜';
html += '
';
if (!status) {
html += '
noch keine Werte gelesen
';
html += '
';
return html;
}
const frei = formatZahl(status.apFrei);
const investiert = formatZahl(status.apInvestiert);
const gesamt = formatZahl(status.apGesamt);
const tooltip = 'AP frei: ' + frei + '\nAP investiert: ' + investiert + '\nAP gesamt: ' + gesamt;
html += '';
html += '' + esc(frei) + '';
html += ' / ';
html += '' + esc(investiert) + '';
html += ' / ';
html += '' + esc(gesamt) + '';
html += '
';
if (status.apNaechsterReset !== null && status.apNaechsterReset !== undefined) {
html += 'Beim nächsten Reset: '
+ esc(formatZahl(status.apNaechsterReset)) + ' AP (Horizon-Wert)
';
}
html += '';
return html;
}
function baueNavigationHtml() {
const aktuell = ermittleAktuellenBereich();
const eintraege = [
{ key: 'building', label: 'Gebäude', icon: '🏢', url: 'https://horiversum.org/game/main/main.php?cmd=building' },
{ key: 'research', label: 'Forschung', icon: '🔬', url: 'https://horiversum.org/game/main/main.php?cmd=research' },
{ key: 'shipyard', label: 'Werft', icon: '🚀', url: 'https://horiversum.org/game/main/main.php?cmd=shipyard' },
{ key: 'fleet', label: 'Flotte', icon: '🎯', url: 'https://horiversum.org/game/main/main.php?cmd=fleet' }
];
let html = '';
html += '';
return html;
}
function baueBereichsanalyseHtml() {
const bereich = ermittleAktuellenBereich();
const kandidaten = findeBauKandidaten();
const laufend = kandidaten.filter(function (k) { return k.info.status === 'laeuft'; });
const baubar = kandidaten.filter(function (k) { return k.info.status === 'baubar'; });
let titel = 'Aktueller Bereich';
if (bereich === 'building' || bereich === 'gebäude') titel = 'Gebäude';
else if (bereich === 'research' || bereich === 'forschung') titel = 'Forschung';
else if (bereich === 'shipyard') titel = 'Werft';
else if (bereich === 'fleet') titel = 'Flotte';
let html = '';
html += '';
html += '
';
html += '
📊 Bereich
';
html += '
' + esc(titel) + '
';
html += '
';
if (bereich === 'building' || bereich === 'gebäude'
|| bereich === 'research' || bereich === 'forschung') {
const laengste = laufend.reduce(function (max, k) {
const sec = Number(k.info.sekunden);
return Number.isFinite(sec) && sec > max ? sec : max;
}, 0);
html += '
';
html += '
' + esc(laufend.length) + '
';
html += '
laufend
';
html += '
' + esc(baubar.length) + '
';
html += '
baubar
';
html += '
'
+ esc(laengste > 0 ? formatSekunden(laengste) : '–') + '
';
html += '
längste Restzeit
';
html += '
';
} else {
html += '
';
html += 'Kontextanzeige für diesen Bereich folgt später.';
html += '
';
}
html += '
';
return html;
}
// ==========================================================
// HASA 1.0.2b – Forschungsplanung mit Forschungslistenfix
// ==========================================================
function techTreeRequest(url) {
return new Promise(function (resolve, reject) {
GM_xmlhttpRequest({
method: 'GET',
url: url,
timeout: 20000,
headers: { 'Accept': 'text/html,application/xhtml+xml' },
onload: function (response) {
if (response.status >= 200 && response.status < 300) {
resolve(response.responseText || '');
} else {
reject(new Error('HTTP ' + response.status));
}
},
ontimeout: function () { reject(new Error('Zeitüberschreitung')); },
onerror: function () { reject(new Error('Netzwerkfehler')); }
});
});
}
function normalisiereTechTreeText(textWert) {
return String(textWert || '').replace(/\u00a0/g, ' ').replace(/\s+/g, ' ').trim();
}
function normalisiereTechTreeName(name) {
return normalisiereTechTreeText(name)
.toLowerCase()
.replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue').replace(/ß/g, 'ss')
.replace(/[^a-z0-9]+/g, ' ').trim();
}
function techTreeTypLabel(typ) {
return ({ building: 'Gebäude', research: 'Forschung', ship: 'Schiffe', defense: 'Verteidigung', unknown: 'Unbekannt' })[typ] || 'Unbekannt';
}
function techTreeTypSymbol(typ) {
return ({ building: '🏢', research: '🔬', ship: '🚀', defense: '🛡️', unknown: '•' })[typ] || '•';
}
function ermittleTechTreeTypAusStan(id, name) {
const daten = laden(STORAGE_STAN_STAMMDATEN, null);
if (!daten || !Array.isArray(daten.objects)) return 'unknown';
const suchId = String(id || '').trim().toUpperCase();
const suchName = normalisiereTechTreeName(name);
let treffer = null;
if (suchId) {
treffer = daten.objects.find(function (o) {
return String(o && o.id || '').trim().toUpperCase() === suchId;
}) || null;
}
if (!treffer && suchName) {
const kandidaten = daten.objects.filter(function (o) {
return normalisiereTechTreeName(o && o.id) === suchName || normalisiereTechTreeName(o && o.name) === suchName;
});
if (kandidaten.length === 1) treffer = kandidaten[0];
}
const typ = treffer && String(treffer.type || '').toLowerCase();
return ['building', 'research', 'ship', 'defense'].includes(typ) ? typ : 'unknown';
}
function ermittleTechTreeTypAusIdUndName(id, name) {
const kennung = String(id || '').toUpperCase();
const n = normalisiereTechTreeName(name);
// Gebäude mit irreführenden technischen IDs zuerst fest zuordnen.
// Insbesondere ist SHIPYARD eine Raumschiffswerft und kein Schiff.
if (/^(SHIPYARD|RESEARCHCENTER|COMPUTERCENTER|CONSTRUCTIONFACILITY|HEADQUARTER|INFRASTRUCTURE|TRADECENTER|SPACEPORT|SPACESTATION|METALMINE|CRYSTALMINE|FUELREFINERY|METALSTORAGE|CRYSTALSTORAGE|FUELSTORAGE|SOLARCOLLECTOR|ROBOTFACTORY|HABITATION|FISSIONREACTOR|FUSIONREACTOR|TIDALPOWERPLANT|RECYCLINGFACILITY|RESCUESTATION|SPACEOBSERVATORY)$/.test(kennung)) return 'building';
if (/^(BLD|BUILD|STOR|SOL|HAB|ROB|HQ|LAB|YARD)/.test(kennung)) return 'building';
if (/^(DEF|TUR|SHIELD|BUNK)/.test(kennung)) return 'defense';
if (/^(RES|TECH|PHY|COMP|ENER|PSY|MAT|MINE|SOLT)/.test(kennung)) return 'research';
if (/^(SCOL|CCOL|BCOL|COL|FIGHT|TRANS|CARGO)/.test(kennung)) return 'ship';
// Verbindliche Spielregel: deutsche Namen mit der Endung „-technik“ sind Forschungen.
// Diese Prüfung muss vor Gebäudebegriffen wie „Roboter“ oder „Solar“ stehen.
if (/technik$/.test(n)) return 'research';
if (/forschung|physik|computer|energie|psychologie|material|minentechnik/.test(n)) return 'research';
if (/werft|mine|lager|kraftwerk|wohnbezirk|roboterfabrik|zentrum|verwaltung|bauhof|forschungseinrichtung|forschungslabor|raumhafen|raumstation|fabrik|hauptquartier|infrastruktur|sternwarte/.test(n)) return 'building';
if (/schiff|kolonieschiff|frachter|jaeger|kreuzer/.test(n)) return 'ship';
if (/verteidigung|geschuetz|schild|bunker/.test(n)) return 'defense';
return 'unknown';
}
function ermittleTechTreeTypAusKontext(element) {
const typen = [
{ muster: /^(Gebäude|Gebaeude)$/i, typ: 'building' },
{ muster: /^Forschungen?$/i, typ: 'research' },
{ muster: /^(Raumschiffe?|Schiffe?)$/i, typ: 'ship' },
{ muster: /^Verteidigung$/i, typ: 'defense' }
];
let knoten = element;
for (let runde = 0; runde < 80 && knoten; runde++) {
let vorher = knoten.previousElementSibling;
while (vorher) {
const vorherText = normalisiereTechTreeText(vorher.textContent);
for (const eintrag of typen) {
if (eintrag.muster.test(vorherText)) return eintrag.typ;
}
const treffer = typen.find(function (eintrag) {
return Array.from(vorher.querySelectorAll('h1,h2,h3,h4,h5,th,strong,b')).some(function (h) {
return eintrag.muster.test(normalisiereTechTreeText(h.textContent));
});
});
if (treffer) return treffer.typ;
vorher = vorher.previousElementSibling;
}
knoten = knoten.parentElement;
}
return 'unknown';
}
function ermittleStufeNebenLink(link) {
const kandidaten = [];
const zeile = link.closest('tr');
if (zeile) kandidaten.push(zeile.textContent);
const parent = link.parentElement;
if (parent) {
kandidaten.push(parent.textContent);
if (parent.nextElementSibling) kandidaten.push(parent.nextElementSibling.textContent);
}
if (link.nextSibling) kandidaten.push(link.nextSibling.textContent);
for (const roh of kandidaten) {
const t = normalisiereTechTreeText(roh);
let m = t.match(/(?:Stufe|Level|Lvl\.?|Ausbaustufe)\s*:?\s*(\d+)/i);
if (m) return Number(m[1]);
m = t.match(/(?:>=|≥)\s*(\d+)/);
if (m) return Number(m[1]);
const zahlen = t.match(/\b\d+\b/g);
if (zahlen && zahlen.length) return Number(zahlen[zahlen.length - 1]);
}
return null;
}
function ermittleTechTreeStufenmarkerFuerLink(link) {
// Der Community-TechTree enthält zwei Richtungen:
// „Stufe N“ = Voraussetzungen dieses Objekts (Vorwärtspfad)
// „ab Stufe N“ = Reverse-Liste: wofür dieses Objekt später gebraucht wird
// Für die Zielplanung darf ausschließlich „Stufe N“ verwendet werden.
if (!link || !link.ownerDocument) return null;
function markerAusElement(element) {
if (!element || element === link || element.contains(link)) return null;
const t = normalisiereTechTreeText(element.textContent);
if (!t || t.length > 100) return null;
let m = t.match(/^ab\s+Stufe\s*:?[ \t]*(\d+)\b/i);
if (m) return { level: Number(m[1]), reverse: true, text: t };
m = t.match(/^Stufen?\s*:?[ \t]*(\d+)(?:\s*[-–—]\s*\d+)?\b/i);
if (m) return { level: Number(m[1]), reverse: false, text: t };
return null;
}
const doc = link.ownerDocument;
const elemente = Array.from(doc.querySelectorAll('h1,h2,h3,h4,h5,h6,legend,strong,b,th,div,span'));
let letzter = null;
for (const el of elemente) {
const lage = el.compareDocumentPosition(link);
if (!(lage & Node.DOCUMENT_POSITION_FOLLOWING)) continue;
const marker = markerAusElement(el);
if (marker) letzter = marker;
}
return letzter;
}
function istTechTreeReverseKontext(link) {
let el = link;
for (let i = 0; i < 10 && el; i++, el = el.parentElement) {
const kennung = [el.id, el.className, el.getAttribute && el.getAttribute('data-tab'), el.getAttribute && el.getAttribute('aria-label')]
.filter(Boolean).join(' ').toLowerCase();
if (/reverse|revers|rueckwaerts|rückwärts/.test(kennung)) return true;
if (el.hidden || (el.getAttribute && el.getAttribute('aria-hidden') === 'true')) return true;
}
return false;
}
function parseTechTreeObjekt(html, erwarteteId) {
const doc = new DOMParser().parseFromString(html, 'text/html');
const bodyText = normalisiereTechTreeText(doc.body ? doc.body.textContent : '');
const titelTreffer = bodyText.match(/Details\s*-\s*(.+?)\s*\(([A-Z0-9_-]+)\)\s*\[\s*Finale Stufe:\s*(\d+)\s*\]/i);
const objektId = titelTreffer ? titelTreffer[2].toUpperCase() : String(erwarteteId || '').toUpperCase();
const name = titelTreffer ? normalisiereTechTreeText(titelTreffer[1]) : objektId;
const finaleStufe = titelTreffer ? Number(titelTreffer[3]) : null;
const requirements = [];
const gesehen = new Set();
Array.from(doc.querySelectorAll('a[href*="/objects/"][href*="/id/"]')).forEach(function (link) {
const href = link.getAttribute('href') || '';
const idTreffer = href.match(/\/objects\/(?:[^/?#]+\/)*?id\/([^/?#]+)/i);
if (!idTreffer) return;
const id = decodeURIComponent(idTreffer[1]).toUpperCase();
if (!id || id === objektId) return;
if (istTechTreeReverseKontext(link)) return;
const marker = ermittleTechTreeStufenmarkerFuerLink(link);
// 1.1 poliert – allgemeiner Gebäude-Fix:
// Der Community-TechTree beschriftet die BASIS-Voraussetzungen vieler
// Gebäude nicht ausdrücklich mit „Stufe 1“. Zusätzliche Anforderungen
// höherer Stufen tragen dagegen einen Stufenmarker (z. B. Stufe 19).
// Markerlose echte Tabellenzeilen sind daher Grundvoraussetzungen für Stufe 1.
if (marker && marker.reverse) return;
const level = ermittleStufeNebenLink(link);
if (!Number.isFinite(level)) return;
let appliesToLevel = null;
if (marker && Number.isFinite(marker.level)) {
appliesToLevel = marker.level;
} else {
const zeile = link.closest('tr');
if (!zeile) return;
appliesToLevel = 1;
}
const eindeutig = appliesToLevel + ':' + id + ':' + level;
if (gesehen.has(eindeutig)) return;
const reqName = normalisiereTechTreeText(link.textContent) || id;
let typ = ermittleTechTreeTypAusStan(id, reqName);
if (typ === 'unknown') typ = ermittleTechTreeTypAusIdUndName(id, reqName);
if (typ === 'unknown') typ = ermittleTechTreeTypAusKontext(link);
gesehen.add(eindeutig);
requirements.push({ id: id, name: reqName, type: typ, level: level, appliesToLevel: appliesToLevel });
});
return {
id: objektId,
name: name,
type: ermittleTechTreeTypAusIdUndName(objektId, name),
finalLevel: finaleStufe,
requirements: requirements,
sourceUrl: TECHTREE_BASIS_URL + encodeURIComponent(String(objektId || '').toLowerCase()),
fetchedAt: Date.now()
};
}
function ermittleTechTreeStufeAusKopf(kopf) {
if (!kopf) return null;
const body = findeKoerperZeileZumKopf(kopf);
const container = kopf.closest('form, .BldTable, .BldContainerAccordionBox') || kopf.parentElement;
const texte = [text(kopf), text(body), text(container)].filter(Boolean);
const muster = [
/(?:Ausbaustufe|Forschungsstufe|Forschungslevel|Stufe|Level|Lvl\.?)\s*:?\s*(\d+)/i,
/\(\s*(?:Ausbaustufe|Forschungsstufe|Forschungslevel|Stufe|Level|Lvl\.?)\s*:?\s*(\d+)\s*\)/i,
/(?:aktuell|derzeit|erforscht)\s*:?\s*(\d+)/i
];
for (const roh of texte) {
for (const regex of muster) {
const treffer = roh.match(regex);
if (treffer) return Number(treffer[1]);
}
}
// Horizon zeigt die Stufe teilweise nur als erste reine Zahl in einer
// Funktionszeile. Nur kleine plausible Stufen akzeptieren, niemals Kosten.
const funktionsWerte = container
? Array.from(container.querySelectorAll('.BldFunctionRight, .BldFunction, td')).map(text)
: [];
for (const wert of funktionsWerte) {
const m = wert.match(/^\s*(\d{1,3})\s*$/);
if (m && Number(m[1]) <= 100) return Number(m[1]);
}
return null;
}
function leseTechTreeNameAusKopf(kopf) {
if (!kopf) return '';
const fett = kopf.querySelector('b, strong');
let name = text(fett) || leseGebaeudeNameAusKopf(kopf) || text(kopf);
name = String(name)
.replace(/\s*[-–]\s*\d[\d'.]*\s*(?:Tonnen-?Typ|ts).*$/i, '')
.replace(/\s*\([A-Z0-9_-]{2,8}\)\s*$/i, '')
.replace(/\s*\((?:Ausbaustufe|Forschungsstufe|Stufe|Level)\s*:?\s*\d+\)\s*$/i, '')
.trim();
return normalisiereGebaeudename(name);
}
function ermittleTechnischeIdAusKopf(kopf) {
if (!kopf) return '';
const container = kopf.closest('form, .BldTable, .BldContainerAccordionBox, .ResContainerAccordionBox') || kopf.parentElement;
const bild = container && container.querySelector('img[alt]');
const alt = bild ? String(bild.getAttribute('alt') || '').trim().toUpperCase() : '';
if (/^[A-Z0-9_-]{2,40}$/.test(alt)) return alt;
const datenId = String((kopf.dataset && (kopf.dataset.id || kopf.dataset.object || kopf.dataset.key)) || '').trim().toUpperCase();
return /^[A-Z0-9_-]{2,40}$/.test(datenId) ? datenId : '';
}
function speichereTechTreeSpielstandAusSeite() {
const bereich = ermittleAktuellenBereich();
const typ = (bereich === 'research' || bereich === 'forschung') ? 'research'
: ((bereich === 'building' || bereich === 'gebäude') ? 'building'
: (bereich === 'shipyard' ? 'ship' : ''));
if (!typ) return 0;
const stand = laden(STORAGE_TECHTREE_SPIELSTAND, {});
const neu = stand && typeof stand === 'object' && !Array.isArray(stand) ? stand : {};
// 1.0.3l: Die aktuell geöffnete Horizon-Seite ist für ihren Bereich die Wahrheit.
// Alte Werte dieses Typs werden vor dem Neu-Einlesen entfernt. Dadurch können
// Forschungs- und Gebäudestände aus dem Leben vor einer Ascension nicht
// weiter als "vorhanden" im Plan stehen bleiben.
Object.keys(neu).forEach(function (key) {
const eintrag = neu[key];
if (eintrag && eintrag.type === typ) delete neu[key];
});
let gefunden = 0;
const koepfe = Array.from(document.querySelectorAll('.BldClsRow, .ResClsRow, [class*="ClsRow"]'));
koepfe.forEach(function (kopf) {
if (!kopf || kopf.closest('#haa-box')) return;
const name = leseTechTreeNameAusKopf(kopf);
let level = ermittleTechTreeStufeAusKopf(kopf);
if (typ === 'ship' && !Number.isFinite(level)) level = 1;
if (!name || name === 'Bauauftrag' || !Number.isFinite(level)) return;
const technischeId = ermittleTechnischeIdAusKopf(kopf);
const eintrag = { id: technischeId || '', name: name, level: level, type: typ, seenAt: Date.now() };
const schluessel = normalisiereTechTreeName(name);
neu[schluessel] = eintrag;
if (technischeId) neu['id:' + technischeId] = eintrag;
const kurz = normalisiereTechTreeName(name
.replace(/^(grundlagen der|grundlagen von|erweiterte|allgemeine)\s+/i, '')
.replace(/\s+(forschung|technik)$/i, ''));
if (kurz && kurz !== schluessel) neu[kurz] = eintrag;
gefunden++;
});
// 1.0.3j: Horizon-Forschungsseite zusätzlich direkt aus den sichtbaren
// Forschungs-Kopfzeilen lesen. Das ist absichtlich unabhängig von den
// CSS-Klassennamen, weil Horizon hier unterschiedliche Row-Klassen nutzt.
//
// Beispiel:
// Physik 16 (physics - Forschungsstufe 16)
//
// Wichtig: Nur auf der echten Forschungsseite anwenden. Der Ausdruck
// „Forschungsstufe“ verhindert außerdem, dass HASA-eigene Planzeilen
// versehentlich als Spielstand eingelesen werden.
if (typ === 'research') {
const sichtText = document.body ? document.body.innerText : '';
const zeilen = sichtText.split(/\n+/)
.map(function (z) { return z.replace(/\s+/g, ' ').trim(); })
.filter(Boolean);
zeilen.forEach(function (zeile) {
const m = zeile.match(/^(.{1,100}?)\s+(\d{1,3})\s*\(\s*([a-z0-9_-]{2,40})\s*-\s*Forschungsstufe\s+(\d{1,3})\s*\)/i);
if (!m) return;
const name = normalisiereGebaeudename(m[1]);
const technischeId = String(m[3] || '').trim().toUpperCase();
const level = Number(m[4]);
if (!name || !technischeId || !Number.isFinite(level)) return;
const eintrag = {
id: technischeId,
name: name,
level: level,
type: 'research',
seenAt: Date.now(),
quelle: 'horizon-forschungs-kopfzeile'
};
const schluessel = normalisiereTechTreeName(name);
neu[schluessel] = eintrag;
neu['id:' + technischeId] = eintrag;
const kurz = normalisiereTechTreeName(name
.replace(/^(grundlagen der|grundlagen von|erweiterte|allgemeine)\s+/i, '')
.replace(/\s+(forschung|technik)$/i, ''));
if (kurz && kurz !== schluessel) neu[kurz] = eintrag;
gefunden++;
});
}
if (typ === 'building') {
const sichtText = document.body ? document.body.innerText : '';
sichtText.split(/\n+/).map(z => z.replace(/\s+/g, ' ').trim()).filter(Boolean).forEach(function (zeile) {
const m = zeile.match(/^(.{1,120}?)\s*\(\s*([a-z0-9_-]{2,50})\s*-\s*Ausbaustufe\s+(\d{1,3})\s*\)/i);
if (!m) return;
const name = normalisiereGebaeudename(m[1]);
const technischeId = String(m[2] || '').trim().toUpperCase();
const level = Number(m[3]);
if (!name || !technischeId || !Number.isFinite(level)) return;
const eintrag = { id: technischeId, name, level, type: 'building', seenAt: Date.now(), quelle: 'horizon-gebaeude-kopfzeile' };
neu['id:' + technischeId] = eintrag;
neu[normalisiereTechTreeName(name)] = eintrag;
gefunden++;
});
}
if (gefunden) speichern(STORAGE_TECHTREE_SPIELSTAND, neu);
return gefunden;
}
function ermittleErfuellungsstand(req) {
const stand = laden(STORAGE_TECHTREE_SPIELSTAND, {});
const reqId = String(req && req.id || '').trim().toUpperCase();
const zielName = normalisiereTechTreeName(req.name);
let eintrag = reqId && stand ? stand['id:' + reqId] : null;
if (!eintrag) eintrag = stand && stand[zielName];
if (!eintrag && stand && typeof stand === 'object') {
const zielKurz = zielName.replace(/^(grundlagen der|grundlagen von|erweiterte|allgemeine)\s+/, '')
.replace(/\s+(forschung|technik)$/, '');
const kandidaten = Object.keys(stand).filter(function (key) {
if (key.indexOf('id:') === 0 || !key || key.length < 4 || zielKurz.length < 4) return false;
return key === zielKurz || key.includes(zielKurz) || zielKurz.includes(key);
});
if (kandidaten.length === 1) eintrag = stand[kandidaten[0]];
}
const aktuell = eintrag && Number.isFinite(Number(eintrag.level)) ? Number(eintrag.level) : null;
return { aktuell: aktuell, erledigt: aktuell !== null && aktuell >= Number(req.level) };
}
function normalisiereStanKategorie(roh) {
const t = normalisiereTechTreeText(roh).toLowerCase();
if (/geb[aä]ude|building/.test(t)) return 'building';
if (/forschung|research|technik/.test(t)) return 'research';
if (/schiff|ship|flotte/.test(t)) return 'ship';
if (/verteidigung|defen[cs]e|abwehr/.test(t)) return 'defense';
return 'unknown';
}
function ermittleStanDatenstandAusDateiname(name) {
const m = String(name || '').match(/(20\d{6})/);
return m ? m[1] : '';
}
function parseStanXml(xmlText, dateiname) {
const doc = new DOMParser().parseFromString(String(xmlText || ''), 'application/xml');
const parserFehler = doc.querySelector('parsererror');
if (parserFehler || !doc.documentElement || doc.documentElement.nodeName.toLowerCase() !== 'statistic') {
throw new Error('Die Datei ist keine lesbare STAN-Statistik-XML.');
}
const bereiche = [
{ tag: 'buildings', type: 'building' },
{ tag: 'researches', type: 'research' },
{ tag: 'ships', type: 'ship' },
{ tag: 'defense', type: 'defense' }
];
const objects = [];
let order = 0;
bereiche.forEach(function (bereich) {
const wurzel = doc.querySelector(bereich.tag);
if (!wurzel) return;
Array.from(wurzel.querySelectorAll(':scope > category')).forEach(function (kat) {
const category = String(kat.getAttribute('name') || '').trim();
Array.from(kat.querySelectorAll(':scope > object')).forEach(function (obj) {
const id = String(obj.getAttribute('name') || '').trim().toUpperCase();
if (!id) return;
const stufen = Array.from(obj.querySelectorAll('count[level]')).map(function (c) { return Number(c.getAttribute('level')); }).filter(Number.isFinite);
objects.push({ id: id, name: id, type: bereich.type, category: category, maxLevel: stufen.length ? Math.max.apply(Math, stufen) : null, order: ++order });
});
});
});
if (!objects.length) throw new Error('Keine Gebäude, Forschungen, Schiffe oder Verteidigungen gefunden.');
return { objects: objects, dataDate: ermittleStanDatenstandAusDateiname(dateiname), fileName: String(dateiname || ''), importedAt: Date.now(), source: 'file' };
}
function vergleicheStanVersionen(alt, neu) {
const altMap = {};
const neuMap = {};
(alt && alt.objects || []).forEach(function (o) { altMap[o.type + ':' + o.id] = o; });
(neu && neu.objects || []).forEach(function (o) { neuMap[o.type + ':' + o.id] = o; });
const added = Object.keys(neuMap).filter(function (k) { return !altMap[k]; }).map(function (k) { return neuMap[k]; });
const missing = Object.keys(altMap).filter(function (k) { return !neuMap[k]; }).map(function (k) { return altMap[k]; });
const changed = Object.keys(neuMap).filter(function (k) {
return altMap[k] && (String(altMap[k].category) !== String(neuMap[k].category) || Number(altMap[k].maxLevel) !== Number(neuMap[k].maxLevel));
}).map(function (k) { return { before: altMap[k], after: neuMap[k] }; });
return { added: added, missing: missing, changed: changed };
}
function importiereStanDatei(datei) {
if (!datei) return Promise.reject(new Error('Keine Datei ausgewählt.'));
speichern(STORAGE_STAN_STATUS, { state: 'loading', message: 'STAN-Datei wird geprüft …', at: Date.now() });
baueBox();
return datei.text().then(function (inhalt) {
const daten = parseStanXml(inhalt, datei.name);
const alt = laden(STORAGE_STAN_STAMMDATEN, null);
const delta = vergleicheStanVersionen(alt, daten);
const versionen = laden(STORAGE_STAN_VERSIONEN, []);
const liste = Array.isArray(versionen) ? versionen : [];
liste.push({ dataDate: daten.dataDate, fileName: daten.fileName, importedAt: daten.importedAt, objects: daten.objects, delta: delta });
speichern(STORAGE_STAN_VERSIONEN, liste.slice(-12));
speichern(STORAGE_STAN_STAMMDATEN, daten);
speichern(STORAGE_STAN_STATUS, {
state: 'ok',
message: daten.objects.length + ' Objekte gespeichert · +' + delta.added.length + ' neu · ' + delta.changed.length + ' geändert · ' + delta.missing.length + ' fehlen im neuen Stand (nicht gelöscht).',
at: Date.now(), dataDate: daten.dataDate, fileName: daten.fileName
});
return daten;
}).catch(function (e) {
speichern(STORAGE_STAN_STATUS, { state: 'error', message: 'STAN-Datei nicht importiert: ' + String(e && e.message || e), at: Date.now() });
throw e;
}).finally(baueBox);
}
function stanAktualitaet(daten) {
if (!daten || !daten.dataDate || !/^20\d{6}$/.test(daten.dataDate)) return { state: 'red', text: 'kein gültiger STAN-Datenstand' };
const y = Number(daten.dataDate.slice(0,4));
const m = Number(daten.dataDate.slice(4,6)) - 1;
const d = Number(daten.dataDate.slice(6,8));
const stand = new Date(y,m,d,12,0,0,0);
const naechster = new Date(stand.getTime() + STAN_RHYTHMUS_TAGE * 86400000);
const jetzt = new Date();
if (jetzt < naechster) return { state: 'green', text: 'aktuell bis ' + naechster.toLocaleDateString('de-DE') };
const tage = Math.floor((jetzt - naechster) / 86400000);
if (tage <= 1) return { state: 'yellow', text: 'neuer Wochenstand könnte verfügbar sein' };
return { state: 'red', text: 'Aktualisierung seit ' + naechster.toLocaleDateString('de-DE') + ' überfällig' };
}
function ladeStanAuswahl() {
const a = laden(STORAGE_STAN_AUSWAHL, null);
return a && typeof a === 'object' ? a : null;
}
function techTreeDebugZeile(debug, text) {
if (!Array.isArray(debug)) return;
debug.push(String(text));
if (debug.length > 1200) debug.shift();
}
function waehleVoraussetzungenBisStufe(objekt, zielStufe, debug, tiefe) {
const grenze = Math.max(1, Number(zielStufe) || 1);
const proId = {};
const einzug = ' '.repeat(Math.max(0, Number(tiefe) || 0));
const objektId = objekt && objekt.id || '?';
const objektName = objekt && objekt.name || objektId;
const roh = (objekt && objekt.requirements || []);
techTreeDebugZeile(debug, einzug + 'AUSWAHL ' + objektName + ' [' + objektId + '] bis Stufe ' + grenze + ' · Rohdaten: ' + roh.length);
roh.forEach(function (req, index) {
const giltAb = Math.max(1, Number(req.appliesToLevel) || 1);
const key = req.id || normalisiereTechTreeName(req.name);
const beschreibung = (req.name || req.id || '?') + ' [' + (req.id || '?') + '] benötigt ' + Number(req.level || 0) + ' · gilt ab Objektstufe ' + giltAb;
if (giltAb > grenze) {
techTreeDebugZeile(debug, einzug + ' − VERWORFEN #' + (index + 1) + ': ' + beschreibung + ' (liegt oberhalb der Zielstufe ' + grenze + ')');
return;
}
const alt = proId[key];
if (!alt) {
proId[key] = Object.assign({}, req, { appliesToLevel: giltAb });
techTreeDebugZeile(debug, einzug + ' + ÜBERNOMMEN #' + (index + 1) + ': ' + beschreibung + ' (erste passende Forderung)');
return;
}
if (Number(req.level) > Number(alt.level)) {
techTreeDebugZeile(debug, einzug + ' ~ ERSETZT #' + (index + 1) + ': ' + beschreibung + ' (höher als zuvor benötigt ' + Number(alt.level || 0) + ', bisher galt ab ' + Number(alt.appliesToLevel || 1) + ')');
proId[key] = Object.assign({}, req, { appliesToLevel: giltAb });
} else {
techTreeDebugZeile(debug, einzug + ' = BEHALTEN #' + (index + 1) + ': ' + beschreibung + ' (vorhandene Forderung benötigt bereits ' + Number(alt.level || 0) + ')');
}
});
const ausgewaehlt = Object.values(proId);
techTreeDebugZeile(debug, einzug + 'ERGEBNIS AUSWAHL ' + objektName + ': ' + ausgewaehlt.length + ' Voraussetzung(en)');
return ausgewaehlt;
}
function sammleTechTreeAnforderungen(graph, zielId, zielStufe) {
const debug = [];
const objekt = graph && graph[zielId];
const grenze = Math.max(1, Number(zielStufe) || 1);
techTreeDebugZeile(debug, '=== DIREKTE MINDESTANFORDERUNGEN ===');
techTreeDebugZeile(debug, 'Ziel: ' + String(zielId || '?') + ' · Zielstufe: ' + grenze);
if (!objekt) {
techTreeDebugZeile(debug, '! Zielobjekt ist noch nicht geladen.');
speichern(STORAGE_TECHTREE_DEBUG, { at: Date.now(), zielId: zielId, zielStufe: grenze, lines: debug });
return [];
}
// Teststufe 1:
// KEINE Rekursion. KEINE Reverse-Auswertung. KEINE Unterzweige.
// Wir lesen ausschließlich die direkten Mindestanforderungen des Zielobjekts.
const direkt = waehleVoraussetzungenBisStufe(objekt, grenze, debug, 0);
direkt.forEach(function (req) {
const rid = String(req && req.id || '').toUpperCase();
const stanTyp = ermittleTechTreeTypAusStan(rid, req && req.name);
if (stanTyp !== 'unknown') {
req.type = stanTyp;
req.typ = stanTyp;
} else if (HASA_SICHERE_FORSCHUNGS_IDS.has(rid)) {
req.type = 'research';
req.typ = 'research';
} else if (!req.type || req.type === 'unknown') {
req.type = ermittleTechTreeTypAusIdUndName(rid, req && req.name);
req.typ = req.type;
}
});
techTreeDebugZeile(debug, 'Direkte Anforderungen: ' + direkt.length);
direkt.forEach(function (req, index) {
const stand = ermittleErfuellungsstand(req);
techTreeDebugZeile(
debug,
String(index + 1).padStart(2, '0') + '. ' +
(req.name || req.id) + ' [' + (req.id || '?') + '] · benötigt ' +
Number(req.level || 0) + ' · vorhanden ' +
(stand.aktuell === null ? '?' : stand.aktuell) +
(stand.erledigt ? ' · ERLEDIGT → aus reduzierter Ansicht raus' : ' · OFFEN → Merkliste')
);
});
techTreeDebugZeile(debug, '=== ENDE DIREKTE MINDESTANFORDERUNGEN ===');
speichern(STORAGE_TECHTREE_DEBUG, {
at: Date.now(),
zielId: zielId,
zielStufe: grenze,
lines: debug
});
console.groupCollapsed('[HASA Planer-Neustart] ' + String(zielId) + ' Stufe ' + grenze);
debug.forEach(function (line) { console.log(line); });
console.groupEnd();
return direkt;
}
async function importiereTechTreeZiel(zielId, zielStufe, erzwingen) {
const id = String(zielId || 'CCOL').trim().toUpperCase();
const grenze = Math.max(1, Number(zielStufe) || 1);
let graph = laden(STORAGE_TECHTREE_GRAPH, {});
graph = graph && typeof graph === 'object' && !Array.isArray(graph) ? graph : {};
speichern(STORAGE_TECHTREE_STATUS, {
state: 'loading',
message: id + ' – direkte Mindestanforderungen werden gelesen …',
id: id,
at: Date.now()
});
baueBox();
try {
// Planer-Neustart Teststufe 1:
// Nur die ausgewählte Zielseite lesen. Keine Unterobjekte nachladen.
// Der Community-TechTree führt seine Objekt-IDs in den URLs klein geschrieben
// (z. B. /id/weaponfactory, /id/darkenergytransmitter). Intern arbeitet
// HASA weiterhin mit den stabilen Großbuchstaben-IDs.
const techTreeUrlId = id.toLowerCase();
const html = await techTreeRequest(TECHTREE_BASIS_URL + encodeURIComponent(techTreeUrlId));
const objekt = parseTechTreeObjekt(html, id);
graph[id] = objekt;
speichern(STORAGE_TECHTREE_GRAPH, graph);
const anforderungen = sammleTechTreeAnforderungen(graph, id, grenze);
if (!anforderungen.length) {
const roh = objekt && Array.isArray(objekt.requirements) ? objekt.requirements : [];
if (roh.length) {
speichern(STORAGE_TECHTREE_STATUS, {
state: 'ok',
message: 'Für Zielstufe ' + grenze + ' keine zusätzlichen Mindestanforderungen.',
id: id,
at: Date.now()
});
return objekt;
}
throw new Error('Keine Mindestanforderungen aus dem TechTree gelesen – TechTree-Seite prüfen.');
}
const offen = anforderungen.filter(function (req) {
return !ermittleErfuellungsstand(req).erledigt;
});
speichern(STORAGE_TECHTREE_STATUS, {
state: 'ok',
message: anforderungen.length + ' direkte Mindestanforderungen · ' +
offen.length + ' davon offen · ',
id: id,
at: Date.now()
});
return objekt;
} catch (fehler) {
speichern(STORAGE_TECHTREE_STATUS, {
state: 'error',
message: String(fehler && fehler.message ? fehler.message : fehler),
id: id,
at: Date.now()
});
throw fehler;
} finally {
baueBox();
}
}
function aktivierePlanStatusSticky() {
const box = document.getElementById('haa-box') || document.querySelector('[id^="haa"]');
if (!box) return;
const checkboxen = Array.from(box.querySelectorAll('input[type="checkbox"]'));
const checkbox = checkboxen.find(cb => ((cb.closest('label') || cb.parentElement)?.textContent || '').includes('vollständige Ansicht'));
if (!checkbox) return;
const label = checkbox.closest('label') || checkbox.parentElement;
const host = label && label.parentElement;
if (!host || host.dataset.hasaStickyPlanstatus === '1') return;
host.dataset.hasaStickyPlanstatus = '1';
host.style.position = 'sticky'; host.style.top = '0'; host.style.zIndex = '20'; host.style.background = '#18140f';
}
function aktivierePlanlistenKlappung() {
const box = document.getElementById('haa-box') || document.querySelector('[id^="haa"]');
if (!box) return;
const all = Array.from(box.querySelectorAll('div'));
const liste = all.find(function (el) {
if (el.dataset && el.dataset.hasaPlanListe === '1') return false;
const t = (el.textContent || '');
const own = Array.from(el.children || []).map(function(c){ return c.textContent || ''; }).join(' ');
return t.indexOf('benötigt') >= 0 &&
(t.indexOf('Hyperraumtechnik') >= 0 || t.indexOf('Flottentechnik') >= 0) &&
el.querySelectorAll('div').length >= 2 &&
el.querySelectorAll('button').length <= 2;
});
if (!liste || !liste.parentNode) return;
if (liste.previousElementSibling && liste.previousElementSibling.classList &&
liste.previousElementSibling.classList.contains('haa-plan-klapp-btn')) return;
liste.dataset.hasaPlanListe = '1';
const btn = document.createElement('button');
btn.className = 'haa-plan-klapp-btn';
btn.type = 'button';
btn.style.cssText = 'font-size:11px;margin:4px 0 6px;padding:2px 7px;cursor:pointer;';
const key = 'haa_planliste_eingeklappt';
function setState(zu) {
liste.style.display = zu ? 'none' : '';
btn.textContent = zu ? '▸ Planliste anzeigen' : '▾ Planliste einklappen';
speichern(key, !!zu);
}
btn.addEventListener('click', function(){ setState(liste.style.display !== 'none'); });
liste.parentNode.insertBefore(btn, liste);
setState(!!laden(key, false));
}
function baueStanNavigationHtml() {
const daten = laden(STORAGE_STAN_STAMMDATEN, null);
const status = laden(STORAGE_STAN_STATUS, null);
const auswahl = ladeStanAuswahl();
const auswahlOffen = !auswahl || laden(STORAGE_STAN_AUSWAHL_OFFEN, '0') === '1';
let html = '';
html += '
';
html += 'Import & Datenbestand';
html += '
';
const aktuell = stanAktualitaet(daten);
const aktuellFarbe = aktuell.state === 'green' ? '#7CFF7C' : aktuell.state === 'yellow' ? '#FFD27C' : '#FF8080';
html += '
● ' + esc(aktuell.text) + (daten && daten.dataDate ? ' · Stand ' + esc(daten.dataDate) : '') + '
';
if (status) html += '
' + esc(status.message || '') + '
';
if (!daten || !Array.isArray(daten.objects)) return html + '
';
html += '';
if (auswahl) {
html += '
';
html += '
Gewähltes Ziel
' + esc(auswahl.name) + ' (' + esc(auswahl.id) + ')
';
html += '
';
html += '
';
} else {
html += '
Ziel auswählen
';
}
if (auswahlOffen) {
const typen = ['building', 'research', 'ship', 'defense'];
html += '
';
typen.forEach(function (typ) {
const gruppe = daten.objects.filter(function (o) {
return o.type === typ && !istStanExplorerKategorieAusgeblendet(o.category);
}).sort(function (a,b) { return Number(a.order)-Number(b.order) || a.name.localeCompare(b.name,'de'); });
if (!gruppe.length) return;
const kategorien = [];
const nachKategorie = {};
gruppe.forEach(function (o) {
const kat = o.category || 'Ohne Untergruppe';
if (!nachKategorie[kat]) {
nachKategorie[kat] = [];
kategorien.push(kat);
}
nachKategorie[kat].push(o);
});
html += '
';
html += '' + techTreeTypSymbol(typ) + ' ' + esc(techTreeTypLabel(typ)) + ' (' + gruppe.length + ')
';
html += '';
kategorien.forEach(function (kat) {
const objekte = nachKategorie[kat];
html += '
';
html += '' + esc(kat) + ' (' + objekte.length + ')
';
html += '';
objekte.forEach(function (o) {
const aktiv = auswahl && auswahl.id === o.id && auswahl.type === o.type;
html += '';
});
html += '
';
});
html += '
';
});
html += '
';
}
html += '
';
return html;
}
function baueTechTreeTestHtml() {
const graph = laden(STORAGE_TECHTREE_GRAPH, {});
const auswahl = ladeStanAuswahl();
const zielId = auswahl && auswahl.id;
const zielStufe = Number(auswahl && auswahl.level) || 1;
const objekt = zielId && graph ? graph[zielId] : null;
const status = laden(STORAGE_TECHTREE_STATUS, null);
const erledigteZeigen = laden(STORAGE_TECHTREE_ERLEDIGT_ZEIGEN, '0') === '1';
const alle = objekt ? sammleTechTreeAnforderungen(graph, zielId, zielStufe) : [];
const bewertet = alle.map(function (req) { return Object.assign({}, req, ermittleErfuellungsstand(req)); });
const offen = bewertet.filter(function (req) { return !req.erledigt; });
const erledigt = bewertet.filter(function (req) { return req.erledigt; });
const sichtbar = erledigteZeigen ? bewertet : offen;
let html = baueStanNavigationHtml();
if (!zielId) return html + 'Nach dem STAN-Import ein Ziel auswählen.
';
html += '';
html += '
';
if (status) html += '
' + esc(status.message || '') + '
';
if (objekt) {
html += '
';
html += '
offen: ' + offen.length + ' · erledigt: ' + erledigt.length + '
';
['building','research','ship','defense','unknown'].forEach(function (typ) {
const gruppe = sichtbar.filter(function (req) { return (req.type || 'unknown') === typ; });
if (!gruppe.length) return;
html += '
' + techTreeTypSymbol(typ) + ' ' + esc(techTreeTypLabel(typ)) + '
';
gruppe.sort(function (a,b) { return String(a.name).localeCompare(String(b.name),'de'); }).forEach(function (req) {
html += '
' + (req.erledigt ? '✓' : '➜') + 'benötigt ' + esc(req.level) + (req.aktuell !== null ? '
vorhanden ' + esc(req.aktuell) + '' : '') + '
';
});
});
}
const debugDaten = laden(STORAGE_TECHTREE_DEBUG, null);
if (debugDaten && Array.isArray(debugDaten.lines)) {
html += '
';
html += 'Diagnose
';
html += '';
html += '
';
html += '
' + esc(debugDaten.lines.join('\n')) + '';
html += '
';
}
return html + '
';
}
function verbindeTechTreeTestButtons() {
const stanBtn = document.getElementById('haa-stan-import');
const stanDatei = document.getElementById('haa-stan-datei');
if (stanBtn && stanDatei) stanBtn.onclick = function () { stanDatei.value = ''; stanDatei.click(); };
if (stanDatei) stanDatei.onchange = function () {
const datei = stanDatei.files && stanDatei.files[0];
if (datei) importiereStanDatei(datei).catch(function () {});
};
const auswahlUmschalten = document.getElementById('haa-stan-auswahl-umschalten');
if (auswahlUmschalten) auswahlUmschalten.onclick = function () {
const offen = laden(STORAGE_STAN_AUSWAHL_OFFEN, '0') === '1';
speichern(STORAGE_STAN_AUSWAHL_OFFEN, offen ? '0' : '1');
baueBox();
};
Array.from(document.querySelectorAll('.haa-stan-ziel')).forEach(function (btn) {
btn.onclick = function () {
const auswahl = { id: btn.dataset.id, name: btn.dataset.name, type: btn.dataset.type, level: 1, selectedAt: Date.now() };
speichern(STORAGE_STAN_AUSWAHL, auswahl);
speichern(STORAGE_STAN_NAVIGATION, { category: auswahl.type, targetId: auswahl.id, at: Date.now() });
speichern(STORAGE_STAN_AUSWAHL_OFFEN, '0');
baueBox();
};
});
const btn = document.getElementById('haa-techtree-import');
if (btn) btn.onclick = function () {
const auswahl = ladeStanAuswahl();
if (!auswahl) return;
btn.disabled = true;
importiereTechTreeZiel(auswahl.id, auswahl.level || 1, true).catch(function () {});
};
const erledigt = document.getElementById('haa-techtree-erledigt');
if (erledigt) erledigt.onchange = function () { speichern(STORAGE_TECHTREE_ERLEDIGT_ZEIGEN, erledigt.checked ? '1' : '0'); baueBox(); };
Array.from(document.querySelectorAll('.haa-req-ziel')).forEach(function (zielBtn) {
zielBtn.onclick = function () {
const id = String(zielBtn.dataset.id || '').trim().toUpperCase();
const name = String(zielBtn.dataset.name || id).trim();
const type = String(zielBtn.dataset.type || 'unknown');
const level = Math.max(1, Number(zielBtn.dataset.level) || 1);
if (!id) return;
const neu = { id: id, name: name, type: type, level: level, selectedAt: Date.now(), quelle: 'anforderungsliste' };
speichern(STORAGE_STAN_AUSWAHL, neu);
speichern(STORAGE_STAN_NAVIGATION, { category: type, targetId: id, at: Date.now() });
speichern(STORAGE_STAN_AUSWAHL_OFFEN, '0');
importiereTechTreeZiel(id, level, true).catch(function () {});
baueBox();
};
});
const debugKopieren = document.getElementById('haa-techtree-debug-kopieren');
if (debugKopieren) debugKopieren.onclick = function () {
const el = document.getElementById('haa-techtree-debug-text');
const inhalt = el ? el.textContent : '';
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(inhalt).then(function () {
debugKopieren.textContent = 'kopiert';
setTimeout(function () { debugKopieren.textContent = 'Debug kopieren'; }, 1200);
}).catch(function () {});
}
};
const debugLoeschen = document.getElementById('haa-techtree-debug-loeschen');
if (debugLoeschen) debugLoeschen.onclick = function () {
speichern(STORAGE_TECHTREE_DEBUG, null);
baueBox();
};
}
function baueAccordionBereichHtml(id, titel, inhaltHtml) {
const offen = HAA_UI_AKTIVER_BEREICH === id;
let html = '';
html += '';
html += '' + titel + '
';
html += '' + inhaltHtml + '
';
html += ' ';
return html;
}
function installierePlanScrollHinweise(inhalt) {
if (!inhalt || inhalt.dataset.hasaScrollHinweise === '1') return;
inhalt.dataset.hasaScrollHinweise = '1';
inhalt.style.position = 'relative';
const host = document.createElement('div');
host.className = 'haa-scroll-hinweise';
host.style.cssText = 'position:sticky;top:2px;right:2px;z-index:50;pointer-events:none;height:0;overflow:visible;';
host.innerHTML = ''
+ ''
+ ''
+ ''
+ '';
inhalt.prepend(host);
const buttons = {};
Array.from(host.querySelectorAll('button')).forEach(function (b) {
buttons[b.dataset.r] = b;
b.style.cssText = 'pointer-events:auto;display:none;margin:0 2px;padding:2px 7px;border:2px solid #ff3030;border-radius:4px;background:#650000;color:#fff;font-weight:bold;font-size:18px;line-height:1;cursor:pointer;box-shadow:0 0 5px #000;';
});
function update() {
const eps = 3;
const oben = inhalt.scrollTop > eps;
const unten = inhalt.scrollTop + inhalt.clientHeight < inhalt.scrollHeight - eps;
const links = inhalt.scrollLeft > eps;
const rechts = inhalt.scrollLeft + inhalt.clientWidth < inhalt.scrollWidth - eps;
buttons.up.style.display = oben ? 'inline-block' : 'none';
buttons.down.style.display = unten ? 'inline-block' : 'none';
buttons.left.style.display = links ? 'inline-block' : 'none';
buttons.right.style.display = rechts ? 'inline-block' : 'none';
}
buttons.up.onclick = function () { inhalt.scrollBy({ top: -Math.max(120, inhalt.clientHeight * 0.6), behavior: 'smooth' }); };
buttons.down.onclick = function () { inhalt.scrollBy({ top: Math.max(120, inhalt.clientHeight * 0.6), behavior: 'smooth' }); };
buttons.left.onclick = function () { inhalt.scrollBy({ left: -Math.max(160, inhalt.clientWidth * 0.6), behavior: 'smooth' }); };
buttons.right.onclick = function () { inhalt.scrollBy({ left: Math.max(160, inhalt.clientWidth * 0.6), behavior: 'smooth' }); };
inhalt.addEventListener('scroll', update, { passive: true });
window.addEventListener('resize', update, { passive: true });
setTimeout(update, 0);
setTimeout(update, 250);
}
function verbindeHasaUiButtons(neuAufbauen) {
const klappButton = document.getElementById('haa-box-klappen');
if (klappButton) {
klappButton.onclick = function () {
HAA_UI_BOX_ZU = !HAA_UI_BOX_ZU;
speichern(STORAGE_UI_BOX_ZU, HAA_UI_BOX_ZU ? '1' : '0');
neuAufbauen();
};
}
Array.from(document.querySelectorAll('#haa-box details.haa-accordion')).forEach(function (details) {
details.addEventListener('toggle', function () {
const bereich = details.dataset.haaBereich || '';
if (!details.open) {
if (HAA_UI_AKTIVER_BEREICH === bereich) {
HAA_UI_AKTIVER_BEREICH = '';
speichern(STORAGE_UI_AKTIVER_BEREICH, '');
}
return;
}
HAA_UI_AKTIVER_BEREICH = bereich;
speichern(STORAGE_UI_AKTIVER_BEREICH, bereich);
Array.from(document.querySelectorAll('#haa-box details.haa-accordion')).forEach(function (anderes) {
if (anderes !== details) anderes.open = false;
});
});
});
const scrollStaende = laden(STORAGE_UI_SCROLL, {});
Array.from(document.querySelectorAll('#haa-box .haa-bereich-inhalt')).forEach(function (inhalt) {
const bereich = inhalt.dataset.haaScrollBereich || '';
const gespeichert = scrollStaende && Number(scrollStaende[bereich]);
if (Number.isFinite(gespeichert) && gespeichert > 0) {
inhalt.scrollTop = gespeichert;
}
if (bereich === 'techtree') installierePlanScrollHinweise(inhalt);
inhalt.addEventListener('scroll', function () {
const aktuell = laden(STORAGE_UI_SCROLL, {});
const neu = aktuell && typeof aktuell === 'object' && !Array.isArray(aktuell) ? aktuell : {};
neu[bereich] = Math.round(inhalt.scrollTop);
speichern(STORAGE_UI_SCROLL, neu);
}, { passive: true });
});
}
function baueBox() {
speichereTechTreeSpielstandAusSeite();
const datenLive = leseAscensions();
const ap = leseAP();
const apNaechsterReset = leseAPBeimNaechstenReset();
const ascensionSichtbar = datenLive.length > 0;
const ascStatusAktuell = ascensionSichtbar ? erstelleAscensionStatus(datenLive, ap, apNaechsterReset) : null;
if (ascStatusAktuell) speichereAscensionStatus(ascStatusAktuell);
const ascStatus = ascStatusAktuell || ladeAscensionStatus();
const datenAnzeige = ascensionSichtbar
? datenLive
: (ascStatus && Array.isArray(ascStatus.skills) ? ascStatus.skills : []);
const favoriten = ladeFavoriten(datenAnzeige);
const spalten = ladeSpalten();
const ziel = aktualisiereUeberwachungsStatus();
const forschung = aktualisiereForschungsStatus();
const box = grundBox();
let html = '';
html += '';
const scanStatus = laden(STORAGE_SEITENSCAN_STATUS, { state: 'green', message: 'bereit' });
const scanFarbe = scanStatus.state === 'red' ? '#ff5a5a' : '#62d862';
html += '⭐ HASA ' + esc(HASA_VERSION) + ' ';
html += '';
html += '
';
if (!HAA_UI_BOX_ZU) {
let ascensionHtml = baueAscensionDashboardHtml(ascStatus);
if (datenAnzeige.length) {
ascensionHtml += '';
ascensionHtml += ' | ';
if (spalten.stufe) ascensionHtml += 'St | ';
if (spalten.letzte) ascensionHtml += 'L | ';
if (spalten.kosten) ascensionHtml += 'K | ';
if (spalten.invest) ascensionHtml += 'I | ';
ascensionHtml += '
';
datenAnzeige.filter(function (e) {
return favoriten.includes(e.name);
}).forEach(function (e) {
const apFrei = ascStatus ? ascStatus.apFrei : null;
const kaufbar = apFrei !== null && e.kosten !== null && apFrei >= e.kosten;
const farbe = kaufbar ? '#7CFF7C' : '#FF8080';
ascensionHtml += '';
ascensionHtml += '| ' + (kaufbar ? '✅ ' : '❌ ') + esc(e.name) + ' | ';
if (spalten.stufe) ascensionHtml += '' + esc(e.stufe ?? '-') + ' | ';
if (spalten.letzte) ascensionHtml += '' + esc(e.letzte ?? '-') + ' | ';
if (spalten.kosten) ascensionHtml += '' + esc(e.kostenText || 'N/A') + ' | ';
if (spalten.invest) ascensionHtml += '' + esc(e.invest ?? '-') + ' | ';
ascensionHtml += '
';
});
ascensionHtml += '
';
ascensionHtml += '';
ascensionHtml += '';
ascensionHtml += '';
ascensionHtml += '
';
}
html += baueNavigationHtml();
html += baueAccordionBereichHtml('ascension', '⭐ Ascension', ascensionHtml);
html += baueAccordionBereichHtml('baualarm', '🏗 Baualarm', baueBaualarmHtml(ziel));
html += baueAccordionBereichHtml('forschungsalarm', '🔬 Forschungsalarm', baueForschungsalarmHtml(forschung));
html += baueAccordionBereichHtml('techtree', '🧭 Forschungsplanung', baueTechTreeTestHtml());
}
box.innerHTML = html;
positioniereBox(box);
const auswahl = document.getElementById('haa-auswahl');
if (auswahl) auswahl.onclick = function () { baueAuswahl(datenAnzeige, favoriten); };
const spaltenBtn = document.getElementById('haa-spalten');
if (spaltenBtn) spaltenBtn.onclick = baueSpalten;
verbindeHasaUiButtons(baueBox);
verbindeBaualarmButtons(ziel, baueBox);
verbindeForschungsalarmButtons(forschung, baueBox);
verbindeTechTreeTestButtons();
aktualisiereBauMarkierungen();
}
function baueAuswahl(daten, favoriten) {
const box = grundBox();
let html = 'Favoriten auswählen
';
html += '';
let letzteKat = '';
daten.forEach(e => {
if (e.kategorie !== letzteKat) {
letzteKat = e.kategorie;
html += '
' + esc(letzteKat) + '
';
}
html += '
';
});
html += '
';
html += '';
html += '';
box.innerHTML = html;
positioniereBox(box);
document.getElementById('haa-save').onclick = function () {
const namen = Array.from(document.querySelectorAll('.haa-check:checked')).map(e => e.value);
speichern(STORAGE_FAVORITEN, namen);
baueBox();
};
document.getElementById('haa-back').onclick = baueBox;
}
function baueSpalten() {
const box = grundBox();
const spalten = ladeSpalten();
box.innerHTML =
'Spalten anzeigen
' +
checkbox('stufe', 'St = Stufe', spalten.stufe) +
checkbox('letzte', 'L = letzte Kosten', spalten.letzte) +
checkbox('kosten', 'K = nächste Kosten', spalten.kosten) +
checkbox('invest', 'I = insgesamt investiert', spalten.invest) +
'' +
'';
positioniereBox(box);
document.getElementById('haa-save-spalten').onclick = function () {
const neu = {};
['stufe', 'letzte', 'kosten', 'invest'].forEach(k => {
neu[k] = document.getElementById('haa-col-' + k).checked;
});
speichern(STORAGE_SPALTEN, neu);
baueBox();
};
document.getElementById('haa-back').onclick = baueBox;
}
function checkbox(key, label, checked) {
return '';
}
let HAA_BAUALARM_TIMER = null;
let HAA_BAUALARM_ANZEIGE_TIMER = null;
let HAA_FORSCHUNGSALARM_TIMER = null;
let HAA_FORSCHUNGSANZEIGE_TIMER = null;
let HAA_AUDIO_CTX = null;
let HAA_AUDIO_FREIGEGEBEN = false;
let HAA_START_GELAUFEN = false;
let HAA_DRAG_AKTIV = false;
let HAA_DRAG_START_X = 0;
let HAA_DRAG_START_Y = 0;
let HAA_DRAG_BOX_X = 0;
let HAA_DRAG_BOX_Y = 0;
let HAA_DRAG_BOX = null;
let HAA_VORALARM_AUSGELOEST_LAUF = '';
let HAA_UEBERWACHUNG_LETZTE_SYNC = 0;
let HAA_AUDIO_KEEPALIVE_TIMER = null;
let HAA_ALARM_BANNER_TIMER = null;
let HAA_SYNC_LAEUFT = false;
let HAA_LETZTE_SCAN_DAUER_MS = 0;
let HAA_AUDIO_KEEPALIVE_OSC = null;
let HAA_AUDIO_KEEPALIVE_GAIN = null;
let HAA_AUDIO_NACHHOLEN = 0;
let HAA_UI_BOX_ZU = laden(STORAGE_UI_BOX_ZU, '0') === '1';
let HAA_UI_AKTIVER_BEREICH = laden(STORAGE_UI_AKTIVER_BEREICH, 'ascension') || 'ascension';
/*
* HASA 0.8 Beobachter-Engine
*
* Grundregel:
* HASA besitzt keine eigene Bauzeit mehr.
* Horizon ist die Uhr.
*
* Der Timer ist nur ein Taktgeber:
* - sichtbare Horizon-Zeit lesen
* - Text anzeigen
* - Sekundenwert nur fuer Voralarm pruefen
*
* Kein Date.now()-Countdown, keine Zielzeit als Wahrheit.
*/
const HAA_CC_NAMEN = [
'Koloniezentrum',
'Großes Koloniezentrum',
'Grosses Koloniezentrum',
'Colonycenter',
'Colony Center',
'Kolonialverwaltung',
'Provisorische Kolonialverwaltung'
];
function istBaualarmAn() {
return laden(STORAGE_BAUALARM, '0') === '1';
}
function setBaualarmAn(an) {
speichern(STORAGE_BAUALARM, an ? '1' : '0');
if (!an && HAA_BAUALARM_TIMER) {
clearInterval(HAA_BAUALARM_TIMER);
HAA_BAUALARM_TIMER = null;
}
}
function istCcGebaeude(name) {
const n = String(name || '').toLowerCase();
return HAA_CC_NAMEN.some(cc => n.includes(cc.toLowerCase()));
}
function normalisiereGebaeudename(name) {
const n = String(name || '')
.replace(/^[\s○◉●◌◎⊙]+/, '')
.replace(/\s+/g, ' ')
.trim();
if (!n || /^countdown$/i.test(n) || /^bauauftrag$/i.test(n)) {
return 'Bauauftrag';
}
return n;
}
function zeitStringZuSekunden(zeit) {
const m = String(zeit || '').match(/(\d+):(\d{2}):(\d{2}):(\d{2})s?/);
if (!m) {
return null;
}
const tage = Number(m[1]);
const stunden = Number(m[2]);
const minuten = Number(m[3]);
const sekunden = Number(m[4]);
return (((tage * 24) + stunden) * 60 + minuten) * 60 + sekunden;
}
function kuerzeZeitText(zeit) {
const m = String(zeit || '').match(/(\d+):(\d{2}):(\d{2}):(\d{2})s?/);
if (!m) {
return String(zeit || '').replace(/s$/, '');
}
const tage = Number(m[1]);
const stunden = Number(m[2]);
const minuten = Number(m[3]);
const sekunden = Number(m[4]);
if (tage > 0) {
return tage + ':' + String(stunden).padStart(2, '0') + ':' + String(minuten).padStart(2, '0') + ':' + String(sekunden).padStart(2, '0');
}
if (stunden > 0) {
return stunden + ':' + String(minuten).padStart(2, '0') + ':' + String(sekunden).padStart(2, '0');
}
return minuten + ':' + String(sekunden).padStart(2, '0');
}
function merkeLetztenBauStatus(info) {
if (!info || info.sekunden === null || info.sekunden === undefined) {
return;
}
speichern(STORAGE_BAUSTATUS, {
planet: info.planet || 'aktueller Planet',
gebaeude: normalisiereGebaeudename(info.name || info.gebaeude),
modus: info.modus || 'Bau',
sekunden: Number(info.sekunden),
zeitText: info.zeitText || formatSekunden(info.sekunden),
zuletztGesehen: Date.now(),
quelle: info.quelle || 'sichtbar'
});
}
function ladeLetztenBauStatus() {
const daten = laden(STORAGE_BAUSTATUS, null);
if (!daten || daten.sekunden === null || daten.sekunden === undefined) {
return null;
}
return {
planet: daten.planet || 'aktueller Planet',
name: normalisiereGebaeudename(daten.gebaeude),
gebaeude: normalisiereGebaeudename(daten.gebaeude),
modus: daten.modus || 'Bau',
sekunden: Number(daten.sekunden),
zeitText: daten.zeitText || formatSekunden(Number(daten.sekunden)),
sichtbar: false,
wartet: true,
quelle: 'letzter-sichtbarer-wert'
};
}
function baueBauInfo(name, zeit, quelle) {
const sekunden = zeitStringZuSekunden(zeit);
if (sekunden === null || sekunden < 0) {
return null;
}
return {
name: normalisiereGebaeudename(name),
gebaeude: normalisiereGebaeudename(name),
planet: 'aktueller Planet',
modus: 'Bau',
sekunden: sekunden,
zeitText: kuerzeZeitText(zeit),
sichtbar: true,
wartet: false,
quelle: quelle
};
}
function findeBauInfoAusZeitZeilen(zeilen) {
for (let i = 0; i < zeilen.length; i++) {
const z = zeilen[i];
const mZeit = z.match(/^Zeit:\s*(\d+:\d{2}:\d{2}:\d{2}s?)\s*$/i);
if (!mZeit) {
continue;
}
let name = 'Koloniezentrum';
let rohName = '';
for (let j = i - 1; j >= 0 && j >= i - 10; j--) {
if (/wird gebaut/i.test(zeilen[j])) {
rohName = zeilen[j].replace(/\s*wird gebaut\..*$/i, '').trim();
name = rohName || name;
break;
}
}
const kontext = (rohName + ' ' + zeilen.slice(Math.max(0, i - 10), i + 1).join(' ')).toLowerCase();
if (istCcGebaeude(name) || /colonycenter|koloniezentrum|kolonialverwaltung/i.test(kontext)) {
return baueBauInfo(name, mZeit[1], 'zeit-zeile');
}
}
return null;
}
function findeBauInfoAusKategorieZeilen(bodyText) {
const reKlammerBau = /\(([^()\n]+?)\s*:\s*(\d+:\d{2}:\d{2}:\d{2}s?)\s*\)/g;
let match;
let bester = null;
while ((match = reKlammerBau.exec(bodyText)) !== null) {
const name = match[1].trim();
if (!istCcGebaeude(name) && !/colonycenter|koloniezentrum|kolonialverwaltung/i.test(name)) {
continue;
}
const info = baueBauInfo(name, match[2], 'kategorie-zeile');
if (info && (!bester || info.sekunden < bester.sekunden)) {
bester = info;
}
}
return bester;
}
function findeSichtbarenBauStatus() {
const bodyText = document.body ? document.body.innerText : '';
if (!bodyText) {
return null;
}
const zeilen = bodyText.split(/\n+/).map(z => z.replace(/\s+/g, ' ').trim()).filter(Boolean);
return findeBauInfoAusZeitZeilen(zeilen) || findeBauInfoAusKategorieZeilen(bodyText);
}
function aktualisiereBauStatusAusSeite() {
const live = findeSichtbarenBauStatus();
if (live) {
merkeLetztenBauStatus(live);
return live;
}
return ladeLetztenBauStatus();
}
function berechneGespeicherteRestsekunden(daten) {
if (!daten) {
return null;
}
const basisSekunden = Number(
daten.restBasisSekunden !== undefined
? daten.restBasisSekunden
: daten.sekunden
);
const basisZeit = Number(
daten.restBasisZeit !== undefined
? daten.restBasisZeit
: daten.zuletztGesehen
);
if (!Number.isFinite(basisSekunden)) {
return null;
}
if (!Number.isFinite(basisZeit) || daten.status !== 'laeuft') {
return Math.max(0, basisSekunden);
}
const vergangen = Math.max(0, Math.floor((Date.now() - basisZeit) / 1000));
return Math.max(0, basisSekunden - vergangen);
}
function ladeBauUeberwachung() {
const daten = laden(STORAGE_BAUUEBERWACHUNG, null);
if (!daten || typeof daten !== 'object' || Array.isArray(daten)) {
return null;
}
const name = normalisiereGebaeudename(daten.name || daten.gebaeude || '');
if (!name || name === 'Bauauftrag') {
return null;
}
const sekunden = berechneGespeicherteRestsekunden(daten);
return {
typ: daten.typ || 'ziel',
bereich: daten.bereich || '',
planet: daten.planet || 'Planet nicht erkannt',
name: name,
gebaeude: name,
status: daten.status || (sekunden !== null ? 'laeuft' : 'vorgemerkt'),
sekunden: sekunden,
zeitText: sekunden !== null ? formatSekunden(sekunden) : '',
sichtbar: false,
wartet: false,
quelle: daten.quelle || 'gespeicherte-ueberwachung',
gestartetAm: daten.gestartetAm || null,
ausgewaehltAm: daten.ausgewaehltAm || Date.now(),
zuletztGesehen: daten.zuletztGesehen || Date.now(),
restBasisSekunden: Number.isFinite(Number(daten.restBasisSekunden))
? Number(daten.restBasisSekunden)
: sekunden,
restBasisZeit: Number.isFinite(Number(daten.restBasisZeit))
? Number(daten.restBasisZeit)
: Date.now(),
letzteLiveSekunden: Number.isFinite(Number(daten.letzteLiveSekunden))
? Number(daten.letzteLiveSekunden)
: null
};
}
function speichereBauUeberwachung(info) {
if (!info) {
return null;
}
const sekundenRoh = Number(info.sekunden);
const sekunden = Number.isFinite(sekundenRoh) ? Math.max(0, sekundenRoh) : null;
const status = info.status === 'laeuft' ? 'laeuft' : 'vorgemerkt';
const jetzt = Date.now();
const ziel = {
typ: info.typ || 'ziel',
bereich: info.bereich || ermittleAktuellenBereich(),
planet: info.planet || findeAktuellenPlanetennamen() || 'Planet nicht erkannt',
name: normalisiereGebaeudename(info.name || info.gebaeude || 'Bauauftrag'),
gebaeude: normalisiereGebaeudename(info.name || info.gebaeude || 'Bauauftrag'),
status: status,
sekunden: sekunden,
zeitText: sekunden !== null ? formatSekunden(sekunden) : '',
quelle: info.quelle || 'benutzer-markierung',
gestartetAm: status === 'laeuft' ? jetzt : null,
ausgewaehltAm: jetzt,
zuletztGesehen: jetzt,
restBasisSekunden: status === 'laeuft' ? sekunden : null,
restBasisZeit: status === 'laeuft' ? jetzt : null,
letzteLiveSekunden: status === 'laeuft' ? sekunden : null
};
speichern(STORAGE_BAUUEBERWACHUNG, ziel);
HAA_UEBERWACHUNG_LETZTE_SYNC = 0;
setBaualarmAn(true);
speichern(STORAGE_ALARM_AUSGELOEST, '');
speichern(STORAGE_VORALARM_AUSGELOEST, '');
HAA_VORALARM_AUSGELOEST_LAUF = '';
return ziel;
}
function beendeBauUeberwachung() {
speichern(STORAGE_BAUUEBERWACHUNG, null);
HAA_UEBERWACHUNG_LETZTE_SYNC = 0;
setBaualarmAn(false);
speichern(STORAGE_ALARM_AUSGELOEST, '');
speichern(STORAGE_VORALARM_AUSGELOEST, '');
HAA_VORALARM_AUSGELOEST_LAUF = '';
aktualisiereBauMarkierungen();
}
function bauInfoPasstZuUeberwachung(info, ziel) {
if (!info || !ziel) {
return false;
}
const a = normalisiereGebaeudename(info.name || info.gebaeude).toLowerCase();
const b = normalisiereGebaeudename(ziel.name || ziel.gebaeude).toLowerCase();
const planetA = String(info.planet || '').trim().toLowerCase();
const planetB = String(ziel.planet || '').trim().toLowerCase();
if (!a || !b || !(a === b || a.includes(b) || b.includes(a))) {
return false;
}
const istForschung = String(info.typ || ziel.typ || '').toLowerCase() === 'forschung';
// Forschung gilt reichsweit. Der Planet entscheidet deshalb nicht,
// ob ein gespeicherter Forschungsauftrag zum sichtbaren Auftrag passt.
if (!istForschung
&& planetA && planetB
&& planetA !== 'planet nicht erkannt'
&& planetB !== 'planet nicht erkannt'
&& planetA !== planetB) {
return false;
}
const bereichA = String(info.bereich || '').toLowerCase();
const bereichB = String(ziel.bereich || '').toLowerCase();
if (bereichA && bereichB && bereichA !== bereichB) {
return false;
}
return true;
}
function escapeRegExp(textWert) {
return String(textWert || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function findeGlobalenZielZeitwert(ziel) {
if (!ziel || !ziel.name) {
return null;
}
const name = normalisiereGebaeudename(ziel.name);
const nameRe = new RegExp(escapeRegExp(name), 'i');
const zeitRe = /(\d+:\d{2}:\d{2}:\d{2}s?)/;
const selektoren = [
'tr',
'.BldClsRow',
'.BldClsBody',
'[class*="event" i]',
'[id*="event" i]',
'[class*="order" i]',
'[id*="order" i]'
];
const elemente = Array.from(document.querySelectorAll(selektoren.join(',')))
.slice(0, 1200);
let bester = null;
for (const el of elemente) {
if (!el || el.closest('#haa-box') || el.closest('.haa-watch-circle')) {
continue;
}
const t = text(el);
if (!t || t.length > 350 || !nameRe.test(t)) {
continue;
}
const m = t.match(zeitRe);
if (!m) {
continue;
}
const sekunden = zeitStringZuSekunden(m[1]);
if (sekunden === null) {
continue;
}
if (!bester || sekunden < bester.sekunden) {
bester = {
sekunden: sekunden,
zeitText: kuerzeZeitText(m[1]),
element: el
};
}
}
return bester;
}
function holeUeberwachungsStatusMitSync(force) {
const jetzt = Date.now();
if (!force
&& HAA_UEBERWACHUNG_LETZTE_SYNC
&& jetzt - HAA_UEBERWACHUNG_LETZTE_SYNC < HASA_SYNC_INTERVALL_MS) {
return ladeBauUeberwachung();
}
if (HAA_SYNC_LAEUFT) {
return ladeBauUeberwachung();
}
HAA_SYNC_LAEUFT = true;
const startZeit = performance.now();
try {
HAA_UEBERWACHUNG_LETZTE_SYNC = jetzt;
return aktualisiereUeberwachungsStatus();
} finally {
HAA_LETZTE_SCAN_DAUER_MS = Math.round(performance.now() - startZeit);
HAA_SYNC_LAEUFT = false;
if (HAA_LETZTE_SCAN_DAUER_MS > 250) {
console.warn(
'HASA Performance: Statusscan dauerte '
+ HAA_LETZTE_SCAN_DAUER_MS
+ ' ms.'
);
}
}
}
function aktualisiereUeberwachungsStatus() {
const ziel = ladeBauUeberwachung();
if (!ziel) {
return null;
}
const kandidaten = findeBauKandidaten();
const live = kandidaten.find(function (k) {
return bauInfoPasstZuUeberwachung(k.info, ziel);
});
if (live) {
const info = live.info;
if (ziel.status === 'laeuft' && info.status === 'baubar') {
const fertig = Object.assign({}, ziel, {
status: 'fertig',
sekunden: 0,
zeitText: '0:00',
sichtbar: true,
wartet: false,
quelle: 'ziel-fertig-erkannt',
zuletztGesehen: Date.now()
});
loeseBaualarmAus(fertig);
speichern(STORAGE_BAUUEBERWACHUNG, null);
setBaualarmAn(false);
speichern(STORAGE_VORALARM_AUSGELOEST, '');
HAA_VORALARM_AUSGELOEST_LAUF = '';
setTimeout(function () {
aktualisiereBauMarkierungen();
baueBox();
}, 250);
return null;
}
if (info.status === 'laeuft' && Number.isFinite(Number(info.sekunden))) {
const liveSekunden = Math.max(0, Number(info.sekunden));
const vorherLive = Number(ziel.letzteLiveSekunden);
const jetzt = Date.now();
// Horizon bleibt die Referenz, sobald dort ein kleinerer Wert erscheint.
// Bleibt der DOM-Wert statisch, läuft HASA lokal sekündlich weiter.
const mussNeuSynchronisieren =
ziel.status !== 'laeuft'
|| !Number.isFinite(Number(ziel.restBasisSekunden))
|| !Number.isFinite(Number(ziel.restBasisZeit))
|| (Number.isFinite(vorherLive) && liveSekunden < vorherLive);
const neu = Object.assign({}, ziel, info, {
name: normalisiereGebaeudename(info.name || ziel.name),
gebaeude: normalisiereGebaeudename(info.name || ziel.name),
planet: info.planet || ziel.planet,
bereich: info.bereich || ziel.bereich,
status: 'laeuft',
sichtbar: true,
wartet: false,
quelle: info.quelle || 'explorer',
gestartetAm: ziel.gestartetAm || jetzt,
zuletztGesehen: jetzt,
letzteLiveSekunden: liveSekunden
});
if (mussNeuSynchronisieren) {
neu.restBasisSekunden = liveSekunden;
neu.restBasisZeit = jetzt;
neu.sekunden = liveSekunden;
} else {
neu.restBasisSekunden = ziel.restBasisSekunden;
neu.restBasisZeit = ziel.restBasisZeit;
neu.sekunden = berechneGespeicherteRestsekunden(neu);
}
neu.zeitText = formatSekunden(neu.sekunden);
speichern(STORAGE_BAUUEBERWACHUNG, neu);
return neu;
}
const neu = Object.assign({}, ziel, info, {
name: normalisiereGebaeudename(info.name || ziel.name),
gebaeude: normalisiereGebaeudename(info.name || ziel.name),
planet: info.planet || ziel.planet,
bereich: info.bereich || ziel.bereich,
status: 'vorgemerkt',
sekunden: null,
zeitText: '',
sichtbar: true,
wartet: false,
quelle: info.quelle || 'explorer',
zuletztGesehen: Date.now(),
restBasisSekunden: null,
restBasisZeit: null,
letzteLiveSekunden: null
});
speichern(STORAGE_BAUUEBERWACHUNG, neu);
return neu;
}
const global = findeGlobalenZielZeitwert(ziel);
if (global && ziel.status === 'laeuft') {
const jetzt = Date.now();
const globalSekunden = Math.max(0, Number(global.sekunden));
const synchronisiert = Object.assign({}, ziel, {
sekunden: globalSekunden,
zeitText: formatSekunden(globalSekunden),
sichtbar: true,
wartet: false,
quelle: 'globaler-statusscanner',
zuletztGesehen: jetzt,
restBasisSekunden: globalSekunden,
restBasisZeit: jetzt,
letzteLiveSekunden: globalSekunden
});
speichern(STORAGE_BAUUEBERWACHUNG, synchronisiert);
return synchronisiert;
}
// Auch ohne sichtbare Quelle läuft ein bereits gestarteter Countdown lokal weiter.
const lokalSekunden = berechneGespeicherteRestsekunden(ziel);
return Object.assign({}, ziel, {
sekunden: lokalSekunden,
zeitText: Number.isFinite(Number(lokalSekunden))
? formatSekunden(lokalSekunden)
: '',
sichtbar: false,
wartet: false,
quelle: 'lokaler-fortlauf'
});
}
function formatSekunden(sec) {
sec = Math.max(0, Math.floor(sec));
const d = Math.floor(sec / 86400);
const h = Math.floor((sec % 86400) / 3600);
const m = Math.floor((sec % 3600) / 60);
const s = sec % 60;
if (d > 0) return d + ':' + String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
if (h > 0) return h + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
return m + ':' + String(s).padStart(2, '0');
}
function zeigeBaualarmStatus(info) {
const restEl = document.getElementById('haa-baualarm-rest');
if (!restEl) {
return;
}
if (!info) {
restEl.textContent = 'unbekannt';
return;
}
if (info.status !== 'laeuft' || !Number.isFinite(Number(info.sekunden))) {
restEl.textContent = 'noch nicht gestartet';
} else {
restEl.textContent = info.zeitText || formatSekunden(info.sekunden);
}
}
function starteBaualarmAnzeigeTicker(info) {
if (HAA_BAUALARM_ANZEIGE_TIMER) {
clearInterval(HAA_BAUALARM_ANZEIGE_TIMER);
HAA_BAUALARM_ANZEIGE_TIMER = null;
}
function tick() {
const restEl = document.getElementById('haa-baualarm-rest');
if (!restEl) {
clearInterval(HAA_BAUALARM_ANZEIGE_TIMER);
HAA_BAUALARM_ANZEIGE_TIMER = null;
return;
}
zeigeBaualarmStatus(holeUeberwachungsStatusMitSync(false));
}
zeigeBaualarmStatus(holeUeberwachungsStatusMitSync(true));
HAA_BAUALARM_ANZEIGE_TIMER = setInterval(tick, 1000);
}
function holeAudioContext() {
const AudioCtx = window.AudioContext || window.webkitAudioContext;
if (!AudioCtx) {
return null;
}
if (!HAA_AUDIO_CTX || HAA_AUDIO_CTX.state === 'closed') {
HAA_AUDIO_CTX = new AudioCtx();
}
return HAA_AUDIO_CTX;
}
function istAudioAktiv() {
return !!(
HAA_AUDIO_CTX
&& HAA_AUDIO_CTX.state === 'running'
&& HAA_AUDIO_FREIGEGEBEN
);
}
function starteAudioKeepalive() {
const ctx = holeAudioContext();
if (!ctx || ctx.state !== 'running' || HAA_AUDIO_KEEPALIVE_OSC) {
return;
}
try {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.value = 18;
gain.gain.value = 0.000001;
osc.connect(gain);
gain.connect(ctx.destination);
osc.start();
HAA_AUDIO_KEEPALIVE_OSC = osc;
HAA_AUDIO_KEEPALIVE_GAIN = gain;
} catch (e) {
console.warn('HASA Audio-Keepalive fehlgeschlagen:', e);
}
}
async function schalteAudioFrei() {
const ctx = holeAudioContext();
if (!ctx) {
HAA_AUDIO_FREIGEGEBEN = false;
return false;
}
try {
if (ctx.state === 'suspended') {
await ctx.resume();
}
if (ctx.state !== 'running') {
HAA_AUDIO_FREIGEGEBEN = false;
return false;
}
const osc = ctx.createOscillator();
const gain = ctx.createGain();
gain.gain.value = 0.0001;
osc.connect(gain);
gain.connect(ctx.destination);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.03);
HAA_AUDIO_FREIGEGEBEN = true;
starteAudioKeepalive();
return true;
} catch (e) {
HAA_AUDIO_FREIGEGEBEN = false;
console.warn('HASA Audiofreigabe fehlgeschlagen:', e);
return false;
}
}
async function spieleNachgeholtenAlarm() {
if (!HAA_AUDIO_NACHHOLEN || !istAudioAktiv()) {
return;
}
const anzahl = HAA_AUDIO_NACHHOLEN;
HAA_AUDIO_NACHHOLEN = 0;
for (let i = 0; i < anzahl; i++) {
setTimeout(function () {
hasaBeep();
}, i * 550);
}
}
function installiereAudioFreigabe() {
const handler = function () {
schalteAudioFrei().then(function (ok) {
if (ok) {
spieleNachgeholtenAlarm();
}
});
};
window.addEventListener('pointerdown', handler, { capture: true, passive: true });
window.addEventListener('mousedown', handler, { capture: true, passive: true });
window.addEventListener('contextmenu', handler, { capture: true, passive: true });
window.addEventListener('keydown', handler, { capture: true, passive: true });
window.addEventListener('touchstart', handler, { capture: true, passive: true });
document.addEventListener('visibilitychange', function () {
if (!document.hidden && HAA_AUDIO_CTX && HAA_AUDIO_CTX.state === 'suspended') {
HAA_AUDIO_CTX.resume().then(function () {
HAA_AUDIO_FREIGEGEBEN = HAA_AUDIO_CTX.state === 'running';
if (HAA_AUDIO_FREIGEGEBEN) {
starteAudioKeepalive();
spieleNachgeholtenAlarm();
}
}).catch(function () {});
}
});
}
async function hasaBeep() {
try {
const ctx = holeAudioContext();
if (!ctx) {
HAA_AUDIO_FREIGEGEBEN = false;
return false;
}
if (ctx.state === 'suspended') {
try {
await ctx.resume();
} catch (e) {
HAA_AUDIO_FREIGEGEBEN = false;
return false;
}
}
if (ctx.state !== 'running') {
HAA_AUDIO_FREIGEGEBEN = false;
return false;
}
function ton(start, freq, dauer, laut) {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'square';
osc.frequency.value = freq;
gain.gain.setValueAtTime(0.0001, ctx.currentTime + start);
gain.gain.exponentialRampToValueAtTime(laut, ctx.currentTime + start + 0.02);
gain.gain.setValueAtTime(laut, ctx.currentTime + start + Math.max(0.03, dauer - 0.05));
gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + start + dauer);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start(ctx.currentTime + start);
osc.stop(ctx.currentTime + start + dauer + 0.02);
}
// HASA 1.0.3a: ein einzelner kurzer Alarmton.
ton(0.00, 740, 0.28, 0.24);
HAA_AUDIO_FREIGEGEBEN = true;
starteAudioKeepalive();
return true;
} catch (e) {
HAA_AUDIO_FREIGEGEBEN = false;
console.warn('HASA Ton konnte nicht abgespielt werden:', e);
return false;
}
}
async function spieleAlarmfolge(anzahl) {
const ok = await hasaBeep();
if (!ok) {
HAA_AUDIO_NACHHOLEN = Math.max(HAA_AUDIO_NACHHOLEN, 1);
return false;
}
return true;
}
function zeigeHasaAlarmBanner(textWert, kritisch) {
const alt = document.getElementById('haa-alarm-banner');
if (alt) {
alt.remove();
}
if (HAA_ALARM_BANNER_TIMER) {
clearTimeout(HAA_ALARM_BANNER_TIMER);
HAA_ALARM_BANNER_TIMER = null;
}
const banner = document.createElement('div');
banner.id = 'haa-alarm-banner';
banner.textContent = textWert;
banner.title = 'Zum Schließen anklicken';
banner.style.position = 'fixed';
banner.style.zIndex = '1000001';
banner.style.left = '50%';
banner.style.top = '18px';
banner.style.transform = 'translateX(-50%)';
banner.style.maxWidth = '760px';
banner.style.padding = '12px 18px';
banner.style.border = kritisch ? '3px solid #ff5a5a' : '2px solid #FFD27C';
banner.style.borderRadius = '6px';
banner.style.background = kritisch ? '#4a1515' : '#211d18';
banner.style.color = '#fff';
banner.style.fontFamily = 'Arial, sans-serif';
banner.style.fontSize = '16px';
banner.style.fontWeight = 'bold';
banner.style.boxShadow = '0 0 16px #000';
banner.style.cursor = 'pointer';
banner.onclick = function () {
banner.remove();
if (HAA_ALARM_BANNER_TIMER) {
clearTimeout(HAA_ALARM_BANNER_TIMER);
HAA_ALARM_BANNER_TIMER = null;
}
};
document.body.appendChild(banner);
HAA_ALARM_BANNER_TIMER = setTimeout(function () {
if (banner && banner.parentNode) {
banner.remove();
}
HAA_ALARM_BANNER_TIMER = null;
}, kritisch ? 30000 : 15000);
}
function alarmKey(info) {
if (!info) {
return '';
}
return [
normalisiereGebaeudename(info.name || 'ziel'),
info.planet || '',
info.bereich || '',
String(info.gestartetAm || info.ausgewaehltAm || '')
].join('|');
}
function voralarmKey(info, vorlaufSekunden) {
if (!info) {
return '';
}
return [info.name || 'bau', String(vorlaufSekunden || 0)].join('|');
}
function loeseVoralarmAus(info, vorlaufSekunden) {
const key = voralarmKey(info, vorlaufSekunden);
if (!key) {
return;
}
if (HAA_VORALARM_AUSGELOEST_LAUF === key
|| laden(STORAGE_VORALARM_AUSGELOEST, '') === key) {
return;
}
HAA_VORALARM_AUSGELOEST_LAUF = key;
speichern(STORAGE_VORALARM_AUSGELOEST, key);
const istForschung = info && info.typ === 'forschung';
spieleAlarmfolge(1);
document.title = istForschung
? '🔬 HASA Forschungsvoralarm - Horizon'
: '🔔 HASA Voralarm - Horizon';
zeigeHasaAlarmBanner(
(istForschung ? 'HASA Forschungsvoralarm: ' : 'HASA Voralarm: ')
+ (info && info.name ? info.name : (istForschung ? 'Forschung' : 'Ziel'))
+ ' ist bald fertig.',
false
);
}
function loeseBaualarmAus(info) {
const key = alarmKey(info);
if (key && laden(STORAGE_ALARM_AUSGELOEST, '') === key) {
return;
}
if (key) {
speichern(STORAGE_ALARM_AUSGELOEST, key);
}
const istForschung = info && info.typ === 'forschung';
spieleAlarmfolge(1);
document.title = istForschung
? '🔬 Forschung fertig - Horizon'
: '🔔 Ziel fertig - Horizon';
zeigeHasaAlarmBanner(
(istForschung ? 'HASA Forschungsalarm: ' : 'HASA Alarm: ')
+ (info && info.name ? info.name : (istForschung ? 'Forschung' : 'Ziel'))
+ ' ist fertig.',
true
);
}
function starteBaualarmWennNoetig(info) {
if (HAA_BAUALARM_TIMER) {
clearInterval(HAA_BAUALARM_TIMER);
HAA_BAUALARM_TIMER = null;
}
if (!istBaualarmAn() || !ladeBauUeberwachung()) {
return;
}
HAA_BAUALARM_TIMER = setInterval(function () {
if (!istBaualarmAn() || !ladeBauUeberwachung()) {
clearInterval(HAA_BAUALARM_TIMER);
HAA_BAUALARM_TIMER = null;
return;
}
const aktuell = holeUeberwachungsStatusMitSync(false);
if (!aktuell || aktuell.status !== 'laeuft' || !Number.isFinite(Number(aktuell.sekunden))) {
return;
}
const vorlauf = ladeVorlaufSekunden();
if (vorlauf > 0
&& aktuell.sekunden > HASA_ALARM_VORLAUF_SEKUNDEN
&& aktuell.sekunden <= vorlauf) {
loeseVoralarmAus(aktuell, vorlauf);
}
if (vorlauf > 0 && aktuell.sekunden > vorlauf + 10) {
HAA_VORALARM_AUSGELOEST_LAUF = '';
speichern(STORAGE_VORALARM_AUSGELOEST, '');
}
if (aktuell.sekunden <= HASA_ALARM_VORLAUF_SEKUNDEN) {
clearInterval(HAA_BAUALARM_TIMER);
HAA_BAUALARM_TIMER = null;
loeseBaualarmAus(aktuell);
beendeBauUeberwachung();
setTimeout(baueBox, 500);
}
}, 1000);
}
function baueBaualarmHtml(ziel) {
const vorlauf = ladeVorlaufSekunden();
let html = '';
const istForschung = ziel && String(ziel.typ || '').toLowerCase() === 'forschung';
html += '';
html += '
'
+ (istForschung ? '🔬 Forschungsalarm' : '🎯 Baualarm / Überwachung')
+ '
';
if (ziel) {
html += '
';
html += '
';
html += '
';
html += '
'
+ esc(normalisiereGebaeudename(ziel.name)) + '
';
if (!istForschung) {
html += '
'
+ esc(ziel.planet || 'Planet nicht erkannt') + '
';
}
if (ziel.status === 'laeuft' && Number.isFinite(Number(ziel.sekunden))) {
html += '
'
+ esc(formatSekunden(ziel.sekunden)) + '
';
} else {
html += '
wartet
';
}
html += '
';
html += '
';
html += '
';
html += '
';
} else {
html += '
Kein Ziel ausgewählt.
';
}
html += '
';
html += '';
html += '';
html += '
';
html += '
';
html += '';
html += '';
html += '
';
html += '
';
return html;
}
function verbindeBaualarmButtons(bau, neuAufbauen) {
starteBaualarmAnzeigeTicker(bau);
starteBaualarmWennNoetig(bau);
const alarmBtn = document.getElementById('haa-alarm-toggle');
if (alarmBtn) {
alarmBtn.onclick = async function () {
const zielAktuell = ladeBauUeberwachung();
if (zielAktuell) {
beendeBauUeberwachung();
neuAufbauen();
return;
}
await schalteAudioFrei();
neuAufbauen();
};
}
const zielKreis = document.getElementById('haa-ziel-kreis');
if (zielKreis) {
zielKreis.onclick = async function () {
const frei = await schalteAudioFrei();
if (frei) starteAudioKeepalive();
if (ladeBauUeberwachung()) beendeBauUeberwachung();
neuAufbauen();
};
}
const tonBtn = document.getElementById('haa-ton-test');
if (tonBtn) {
tonBtn.onclick = async function () {
await schalteAudioFrei();
await hasaBeep();
neuAufbauen();
};
}
verbindeZielUndVorlaufButtons(neuAufbauen);
}
function entferneBauMarkierungen() {
document.querySelectorAll('.haa-watch-circle').forEach(function (el) {
el.remove();
});
}
function ermittleAktuellenBereich() {
const aktiv = document.querySelector('.NavLinkActive');
if (aktiv && aktiv.id) {
return aktiv.id.replace(/^nav_/, '').toLowerCase();
}
const m = String(location.search || '').match(/[?&]cmd=([^&]+)/i);
return m ? decodeURIComponent(m[1]).toLowerCase() : 'unbekannt';
}
function findeAktuellenPlanetennamen() {
const selected = document.querySelector('select.PlanetSelectBox option:checked');
if (selected) {
const name = text(selected);
if (name) return name;
}
const select = document.querySelector('select.PlanetSelectBox');
if (select && select.options && select.selectedIndex >= 0) {
const option = select.options[select.selectedIndex];
const name = text(option);
if (name) return name;
}
return 'Planet nicht erkannt';
}
function istElementSichtbar(el) {
if (!el || !el.getBoundingClientRect) return false;
const r = el.getBoundingClientRect();
const cs = window.getComputedStyle(el);
return r.width > 0
&& r.height > 0
&& cs.display !== 'none'
&& cs.visibility !== 'hidden';
}
function leseZeitAusText(textWert, bevorzugtLaufend) {
const t = String(textWert || '').replace(/\s+/g, ' ').trim();
let m = null;
if (bevorzugtLaufend) {
m = t.match(/Zeit:\s*(\d+:\d{2}:\d{2}:\d{2}s?)/i);
}
if (!m) {
m = t.match(/Bauzeit:\s*(\d+:\d{2}:\d{2}:\d{2}s?)/i);
}
if (!m) {
m = t.match(/(\d+:\d{2}:\d{2}:\d{2}s?)/i);
}
return m ? m[1] : '';
}
function leseGebaeudeNameAusKopf(kopf) {
if (!kopf) return '';
const b = kopf.querySelector('b');
if (b) {
const clone = b.cloneNode(true);
clone.querySelectorAll('.haa-watch-circle').forEach(function (el) {
el.remove();
});
const name = normalisiereGebaeudename(text(clone));
if (name) return name;
}
const clone = kopf.cloneNode(true);
clone.querySelectorAll('.haa-watch-circle').forEach(function (el) {
el.remove();
});
let roh = text(clone);
roh = roh.replace(/\s*\([^)]*Ausbaustufe[^)]*\)\s*$/i, '').trim();
roh = roh.replace(/\s*wird gebaut.*$/i, '').trim();
return normalisiereGebaeudename(roh);
}
function findeKoerperZeileZumKopf(kopf) {
const kopfZeile = kopf && kopf.closest ? kopf.closest('tr') : null;
let el = kopfZeile ? kopfZeile.nextElementSibling : null;
while (el) {
if (el.classList && el.classList.contains('BldClsBody')) {
return el;
}
if (el.querySelector && el.querySelector('.BldClsRow')) {
break;
}
el = el.nextElementSibling;
}
return null;
}
function istAktiverBauenButton(body) {
if (!body) return null;
const links = Array.from(body.querySelectorAll('a.SystemGfxButton'));
return links.find(function (a) {
const klassen = a.classList;
return /^bauen$/i.test(text(a))
&& klassen.contains('Green')
&& !klassen.contains('SystemGfxButtonDisabled');
}) || null;
}
function istLaufenderBau(kopf, body) {
const gesamt = (text(kopf) + ' ' + text(body)).replace(/\s+/g, ' ');
const hatWirdGebaut = /wird gebaut/i.test(gesamt);
const hatAbbrechen = Array.from((body || document).querySelectorAll('a, button'))
.some(function (el) { return /^abbrechen$/i.test(text(el)); });
const hatZeit = /Zeit:\s*\d+:\d{2}:\d{2}:\d{2}s?/i.test(gesamt);
return (hatWirdGebaut || hatAbbrechen) && hatZeit;
}
function findeBauKandidaten() {
const planet = findeAktuellenPlanetennamen();
const kandidaten = [];
const gesehen = {};
const koepfe = Array.from(document.querySelectorAll('.BldClsRow'));
koepfe.forEach(function (kopf) {
if (!kopf || kopf.closest('#haa-box')) return;
const body = findeKoerperZeileZumKopf(kopf);
if (!body) return;
const name = normalisiereGebaeudename(leseGebaeudeNameAusKopf(kopf));
if (!name || name === 'Bauauftrag') return;
const laufend = istLaufenderBau(kopf, body);
const bauenButton = istAktiverBauenButton(body);
if (!laufend && !bauenButton) {
return;
}
const zeit = leseZeitAusText(text(kopf) + ' ' + text(body), laufend);
if (!zeit) return;
const sekunden = zeitStringZuSekunden(zeit);
if (sekunden === null) return;
const titelEl = kopf.querySelector('b') || kopf;
const key = planet.toLowerCase() + '|' + name.toLowerCase();
if (gesehen[key]) return;
gesehen[key] = true;
const bereich = ermittleAktuellenBereich();
const typ = bereich === 'forschung' || bereich === 'research'
? 'forschung'
: 'bau';
kandidaten.push({
key: key,
kopf: kopf,
body: body,
titelEl: titelEl,
info: {
name: name,
gebaeude: name,
// Forschung ist global: kein Startplanet in Anzeige oder Zuordnung.
planet: typ === 'forschung' ? '' : planet,
bereich: bereich,
typ: typ,
status: laufend ? 'laeuft' : 'baubar',
modus: 'Ziel',
sekunden: laufend ? sekunden : null,
bauzeitSekunden: laufend ? null : sekunden,
zeitText: kuerzeZeitText(zeit),
sichtbar: true,
wartet: false,
quelle: laufend ? 'horizon-bld-laufend' : 'horizon-bld-baubar'
}
});
});
return kandidaten;
}
function entferneBauMarkierungen() {
document.querySelectorAll('.haa-watch-circle').forEach(function (el) {
el.remove();
});
}
function setzeKreisAnTitel(kandidat, ziel) {
const titelEl = kandidat && kandidat.titelEl;
if (!titelEl || !titelEl.parentNode) return;
const markiert = ziel && bauInfoPasstZuUeberwachung(kandidat.info, ziel);
const btn = document.createElement('button');
btn.className = 'haa-watch-circle';
btn.type = 'button';
btn.textContent = markiert ? '◉' : '○';
const istForschung = String(kandidat.info.typ || '').toLowerCase() === 'forschung';
btn.title = markiert
? 'Überwachung beenden: ' + kandidat.info.name
: (istForschung
? 'Diese Forschung überwachen: ' + kandidat.info.name
: 'Diesen Bau überwachen: ' + kandidat.info.name + ' auf ' + kandidat.info.planet);
btn.style.display = 'inline-flex';
btn.style.alignItems = 'center';
btn.style.justifyContent = 'center';
btn.style.verticalAlign = 'middle';
btn.style.marginRight = '6px';
btn.style.width = '18px';
btn.style.height = '18px';
btn.style.lineHeight = '16px';
btn.style.padding = '0';
btn.style.borderRadius = '50%';
btn.style.border = '1px solid #FFD27C';
btn.style.background = '#211d18';
btn.style.color = markiert ? '#7CFF7C' : '#FFD27C';
btn.style.fontWeight = 'bold';
btn.style.cursor = 'pointer';
btn.style.boxShadow = '0 0 3px #000';
btn.onclick = function (e) {
e.preventDefault();
e.stopPropagation();
schalteAudioFrei();
const aktuell = ladeBauUeberwachung();
if (aktuell && bauInfoPasstZuUeberwachung(kandidat.info, aktuell)) {
beendeBauUeberwachung();
} else {
speichereBauUeberwachung(kandidat.info);
}
baueBox();
setTimeout(aktualisiereBauMarkierungen, 80);
};
titelEl.insertBefore(btn, titelEl.firstChild);
}
function aktualisiereBauMarkierungen() {
entferneBauMarkierungen();
let ziel = ladeBauUeberwachung();
const kandidaten = findeBauKandidaten();
// Forschung wird unabhängig vom Bauziel gespeichert. Dadurch können
// Baualarm und Forschungsalarm gleichzeitig sichtbar und aktiv bleiben.
const laufendeForschungen = kandidaten.filter(function (kandidat) {
return kandidat.info.typ === 'forschung'
&& kandidat.info.status === 'laeuft'
&& Number.isFinite(Number(kandidat.info.sekunden));
});
if (laufendeForschungen.length === 1) {
speichereForschungsUeberwachung(laufendeForschungen[0].info);
}
kandidaten.forEach(function (kandidat) {
setzeKreisAnTitel(kandidat, ziel);
});
}
/*
* hasa 1.0.1b: Forschungsüberwachung läuft vollständig getrennt
* von der Bauüberwachung. So können beide Aufträge parallel bestehen.
*/
function ladeForschungsUeberwachung() {
const daten = laden(STORAGE_FORSCHUNGSUEBERWACHUNG, null);
if (!daten || typeof daten !== 'object' || Array.isArray(daten)) return null;
const name = normalisiereGebaeudename(daten.name || '');
if (!name || name === 'Bauauftrag') return null;
const sekunden = berechneGespeicherteRestsekunden(daten);
return Object.assign({}, daten, {
typ: 'forschung',
bereich: 'forschung',
planet: '',
name: name,
gebaeude: name,
sekunden: sekunden,
zeitText: Number.isFinite(Number(sekunden)) ? formatSekunden(sekunden) : ''
});
}
function speichereForschungsUeberwachung(info) {
if (!info) return null;
const alt = ladeForschungsUeberwachung();
const name = normalisiereGebaeudename(info.name || info.gebaeude || 'Forschung');
const sekundenRoh = Number(info.sekunden);
const sekunden = Number.isFinite(sekundenRoh) ? Math.max(0, sekundenRoh) : null;
const jetzt = Date.now();
// Beim sekündlichen Seitenscan nicht jedes Mal den Startzeitpunkt erneuern.
const gleicherAuftrag = alt && normalisiereGebaeudename(alt.name).toLowerCase() === name.toLowerCase();
const ziel = {
typ: 'forschung',
bereich: 'forschung',
planet: '',
name: name,
gebaeude: name,
status: sekunden !== null ? 'laeuft' : 'vorgemerkt',
sekunden: sekunden,
zeitText: sekunden !== null ? formatSekunden(sekunden) : '',
quelle: info.quelle || 'forschungsseite',
gestartetAm: gleicherAuftrag ? (alt.gestartetAm || jetzt) : jetzt,
ausgewaehltAm: gleicherAuftrag ? (alt.ausgewaehltAm || jetzt) : jetzt,
zuletztGesehen: jetzt,
restBasisSekunden: sekunden,
restBasisZeit: jetzt,
letzteLiveSekunden: sekunden
};
speichern(STORAGE_FORSCHUNGSUEBERWACHUNG, ziel);
setBaualarmAn(true);
return ziel;
}
function beendeForschungsUeberwachung() {
speichern(STORAGE_FORSCHUNGSUEBERWACHUNG, null);
if (HAA_FORSCHUNGSALARM_TIMER) {
clearInterval(HAA_FORSCHUNGSALARM_TIMER);
HAA_FORSCHUNGSALARM_TIMER = null;
}
}
function migriereAlteForschungsUeberwachung() {
const alt = ladeBauUeberwachung();
if (!alt || String(alt.typ || '').toLowerCase() !== 'forschung') return;
speichereForschungsUeberwachung(alt);
speichern(STORAGE_BAUUEBERWACHUNG, null);
}
function findeLaufendeForschungAusSeite() {
const bodyText = document.body ? document.body.innerText : '';
if (!bodyText) return null;
const zeilen = bodyText.split(/\n+/)
.map(function (z) { return z.replace(/\s+/g, ' ').trim(); })
.filter(Boolean);
const zeitRe = /(\d+:\d{2}:\d{2}:\d{2}s?)/;
// Sicherster Hinweis auf der Imperiumsübersicht:
// "Forschungsauftrag: Solarenergietechnik 9 0:02:29:53s"
for (let i = 0; i < zeilen.length; i++) {
if (!/Forschungsauftrag\s*:/i.test(zeilen[i])) continue;
let gesamt = zeilen[i];
for (let j = 1; j <= 2 && i + j < zeilen.length; j++) {
if (!zeitRe.test(gesamt)) gesamt += ' ' + zeilen[i + j];
}
const m = gesamt.match(/Forschungsauftrag\s*:\s*(.+?)\s+(\d+:\d{2}:\d{2}:\d{2}s?)(?:\s|$)/i);
if (!m) continue;
const name = normalisiereGebaeudename(m[1]);
const sekunden = zeitStringZuSekunden(m[2]);
if (!name || sekunden === null) continue;
return {
typ: 'forschung',
bereich: 'forschung',
planet: '',
name: name,
gebaeude: name,
status: 'laeuft',
modus: 'Ziel',
sekunden: sekunden,
zeitText: kuerzeZeitText(m[2]),
sichtbar: true,
wartet: false,
quelle: 'forschungsauftrag-zeile'
};
}
return null;
}
function findeForschungsZeitInEreignissen(ziel) {
if (!ziel || !ziel.name) return null;
const name = normalisiereGebaeudename(ziel.name);
const nameRe = new RegExp(escapeRegExp(name), 'i');
const zeitRe = /(\d+:\d{2}:\d{2}:\d{2}s?)/g;
const kandidaten = [];
function fuegeKandidatHinzu(t, quelle, prioritaet) {
if (!t || t.length > 300 || !nameRe.test(t)) return;
const treffer = Array.from(t.matchAll(zeitRe));
if (!treffer.length) return;
// Eine Horizon-Ereigniszeile enthält normalerweise genau eine Zeit.
// Enthält ein großer Container mehrere Zeiten, ist er als Uhrquelle
// ungeeignet und wird bewusst verworfen.
if (treffer.length !== 1) return;
const sekunden = zeitStringZuSekunden(treffer[0][1]);
if (sekunden === null) return;
kandidaten.push({
sekunden: sekunden,
zeitText: kuerzeZeitText(treffer[0][1]),
quelle: quelle,
prioritaet: prioritaet,
textLaenge: t.length
});
}
// 1. Vorrang: echte Tabellenzeilen der Ereignisliste.
Array.from(document.querySelectorAll('tr')).forEach(function (el) {
if (!el || el.closest('#haa-box')) return;
fuegeKandidatHinzu(text(el), 'ereignis-tabellenzeile', 1);
});
// 2. Danach nur kleine, zeilenartige Elemente. Keine großen DIV-Container,
// weil diese mehrere Countdown-Zeiten enthalten und früher z. B. 9:09
// vom Bauauftrag fälschlich als Forschungszeit lieferten.
Array.from(document.querySelectorAll('li, p, td, [class*="event" i], [id*="event" i]')).forEach(function (el) {
if (!el || el.closest('#haa-box')) return;
const r = el.getBoundingClientRect ? el.getBoundingClientRect() : null;
if (r && r.height > 80) return;
fuegeKandidatHinzu(text(el), 'ereignis-zeilenelement', 2);
});
// 3. Sicherer Text-Fallback: sichtbare Zeilen einzeln prüfen.
if (document.body) {
document.body.innerText.split(/\n+/)
.map(function (z) { return z.replace(/\s+/g, ' ').trim(); })
.filter(Boolean)
.forEach(function (zeile) {
fuegeKandidatHinzu(zeile, 'ereignis-textzeile', 3);
});
}
if (!kandidaten.length) return null;
// Nicht die kleinste Zeit gewinnt, sondern die eindeutigste Horizon-Zeile.
// Bei gleicher Quelle ist die kürzere Textzeile spezifischer.
kandidaten.sort(function (a, b) {
if (a.prioritaet !== b.prioritaet) return a.prioritaet - b.prioritaet;
return a.textLaenge - b.textLaenge;
});
return kandidaten[0];
}
function aktualisiereForschungsStatus() {
let ziel = ladeForschungsUeberwachung();
// Auf der Imperiumsübersicht steht die laufende Forschung ausdrücklich
// hinter "Forschungsauftrag:". Diese Zeile hat Vorrang und funktioniert
// unabhängig von den CSS-Klassen der Ereignisliste.
const direkt = findeLaufendeForschungAusSeite();
if (direkt) {
ziel = speichereForschungsUeberwachung(direkt);
}
// Auf der Forschungsseite übernimmt HASA die einzige laufende Forschung automatisch.
const liveKandidaten = findeBauKandidaten().filter(function (kandidat) {
return kandidat.info.typ === 'forschung'
&& kandidat.info.status === 'laeuft'
&& Number.isFinite(Number(kandidat.info.sekunden));
});
if (liveKandidaten.length === 1) {
ziel = speichereForschungsUeberwachung(liveKandidaten[0].info);
}
if (!ziel) return null;
// Auf allen Seiten wird anschließend die Ereignisliste als Horizon-Uhr benutzt.
const ereignis = findeForschungsZeitInEreignissen(ziel);
if (ereignis) {
const jetzt = Date.now();
const neu = Object.assign({}, ziel, {
status: 'laeuft',
sekunden: ereignis.sekunden,
zeitText: formatSekunden(ereignis.sekunden),
sichtbar: true,
quelle: 'ereignisliste',
zuletztGesehen: jetzt,
restBasisSekunden: ereignis.sekunden,
restBasisZeit: jetzt,
letzteLiveSekunden: ereignis.sekunden
});
speichern(STORAGE_FORSCHUNGSUEBERWACHUNG, neu);
return neu;
}
const lokal = berechneGespeicherteRestsekunden(ziel);
return Object.assign({}, ziel, {
sekunden: lokal,
zeitText: Number.isFinite(Number(lokal)) ? formatSekunden(lokal) : '',
sichtbar: false,
quelle: 'lokaler-fortlauf'
});
}
function baueForschungsalarmHtml(forschung) {
let html = '';
html += '';
html += '
🔬 Forschungsalarm
';
if (forschung) {
html += '
';
html += '
';
html += '
' + esc(forschung.name) + '
';
html += '
'
+ esc(Number.isFinite(Number(forschung.sekunden)) ? formatSekunden(forschung.sekunden) : 'wartet')
+ '
';
html += '
';
html += '
';
} else {
html += '
Keine laufende Forschung erkannt.
';
}
html += '
';
return html;
}
function starteForschungsAnzeigeTicker() {
if (HAA_FORSCHUNGSANZEIGE_TIMER) clearInterval(HAA_FORSCHUNGSANZEIGE_TIMER);
HAA_FORSCHUNGSANZEIGE_TIMER = setInterval(function () {
const el = document.getElementById('haa-forschungsalarm-rest');
if (!el) return;
const aktuell = aktualisiereForschungsStatus();
el.textContent = aktuell && Number.isFinite(Number(aktuell.sekunden))
? formatSekunden(aktuell.sekunden)
: 'unbekannt';
}, 1000);
}
function starteForschungsalarmWennNoetig() {
if (HAA_FORSCHUNGSALARM_TIMER) clearInterval(HAA_FORSCHUNGSALARM_TIMER);
if (!ladeForschungsUeberwachung()) return;
HAA_FORSCHUNGSALARM_TIMER = setInterval(function () {
const aktuell = aktualisiereForschungsStatus();
if (!aktuell || !Number.isFinite(Number(aktuell.sekunden))) return;
// Forschungsalarm: kein minutenfrüher Voralarm. Nur der Hauptalarm
// kurz vor dem sichtbaren Ende bleibt aktiv.
if (aktuell.sekunden <= HASA_ALARM_VORLAUF_SEKUNDEN) {
loeseBaualarmAus(aktuell);
beendeForschungsUeberwachung();
setTimeout(baueBox, 300);
}
}, 1000);
}
function verbindeForschungsalarmButtons(forschung, neuAufbauen) {
starteForschungsAnzeigeTicker();
starteForschungsalarmWennNoetig();
const btn = document.getElementById('haa-forschungsziel-kreis');
if (btn) {
btn.onclick = function () {
beendeForschungsUeberwachung();
neuAufbauen();
};
}
}
let HAA_SEITENSCAN_TIMER = null;
function planeTechTreeSeitenscan(verzoegerung) {
clearTimeout(HAA_SEITENSCAN_TIMER);
speichern(STORAGE_SEITENSCAN_STATUS, { state: 'red', message: 'sichtbare Horizon-Daten werden eingelesen', at: Date.now() });
baueBox();
HAA_SEITENSCAN_TIMER = setTimeout(function () {
const bereich = ermittleAktuellenBereich();
const istDatenSeite = bereich === 'research' || bereich === 'forschung'
|| bereich === 'building' || bereich === 'gebäude' || bereich === 'shipyard';
if (istDatenSeite) speichereTechTreeSpielstandAusSeite();
speichern(STORAGE_SEITENSCAN_STATUS, { state: 'green', message: istDatenSeite ? 'sichtbare Horizon-Daten aktuell' : 'bereit', at: Date.now() });
baueBox();
}, Number(verzoegerung) || 350);
}
function istHorizonKlappkopf(element) {
if (!element || !element.closest) return false;
if (element.closest('#haa-box')) return false;
return !!element.closest(
'.BldContainerHeadBox, .ResContainerHeadBox, '
+ '.BldAccordionHead, .ResAccordionHead, '
+ '[class*="AccordionHead"], [class*="ContainerHeadBox"]'
);
}
function installiereAutomatischenSeitenscan() {
if (document.documentElement.dataset.hasaSeitenscan === '1') return;
document.documentElement.dataset.hasaSeitenscan = '1';
document.addEventListener('click', function (event) {
if (!istHorizonKlappkopf(event.target)) return;
// Horizon klappt den Inhalt erst nach dem Klick auf. Zwei kurze
// Nachscans erfassen sowohl sofort vorhandene als auch verzögert
// eingesetzte Zeilen, ohne andere Horizon-Seiten abzurufen.
planeTechTreeSeitenscan(250);
setTimeout(function () { planeTechTreeSeitenscan(150); }, 650);
}, true);
}
function istAscensionSeite() {
return document.querySelector('.AscAccCategory') !== null;
}
function starteSicher() {
if (HAA_START_GELAUFEN) {
return;
}
HAA_START_GELAUFEN = true;
migriereAlteForschungsUeberwachung();
let versuche = 0;
const timer = setInterval(function () {
versuche++;
if (document.body || versuche >= 8) {
baueBox();
clearInterval(timer);
}
}, 400);
}
installiereAutomatischenSeitenscan();
installiereAudioFreigabe();
window.addEventListener('load', starteSicher);
setTimeout(starteSicher, 1000);
setInterval(aktivierePlanlistenKlappung, 700);
setInterval(aktivierePlanStatusSticky, 700);
})();
/* HASA 1.1 – Forschungsmodul komplett
- STAN-Daten als maßgebliche Quelle für TechTree-Typen
- allgemeine Erkennung von Gebäude, Forschung, Schiff und Verteidigung
- Forschungs-Iststand wird aus der sichtbaren Horizon-Seite übernommen
- Ascension-Reset verhindert veraltete Forschungs- und Gebäudestände
- TechTree-Soll/Ist-Vergleich wird nach erneutem Seitenscan aktualisiert
- responsive HASA-Breite für kleinere Displays
- keine Horizon-Seiten werden im Hintergrund abgerufen
*/