SYSTEMWERTE
…
+…
- + +Wie machst du die Softwarekomponente nutzbar?
+ +Die Spielzeit, Produktion, Scannerkosten und Ereignistimer laufen weiter, sobald dieses Fenster geschlossen wird.
+ + - - + +diff --git a/AGENTS.md b/AGENTS.md index 3be5f40..1dcb814 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,13 @@ Das Spiel soll leicht zugänglich beginnen und seine Komplexität langsam offenb Der Einstieg orientiert sich in seiner Zugänglichkeit an klassischen Incremental-Spielen: Eine offensichtliche aktive Handlung steht am Anfang, neue Ressourcen und Systeme werden einzeln freigeschaltet, und frühe Fortschritte sind durch aktives Klicken deutlich schneller erreichbar als durch reines Warten. Inhalte, Texte, Oberfläche und Grafiken bleiben eigenständig. -Die frühe Entwicklung lautet Bit, Byte, Datenfragment, Subroutine und Prozess. Das Datenfragment erhält mit der Energieroutine eine erste kontinuierliche Impulsproduktion. Ab dem Datenfragment muss die KI eine feste Mindeststruktur besitzen; vorläufig mindestens 8 Bits und 1 Byte, damit die neue Evolutionsstufe nicht leer oder widersprüchlich wirkt. Das erste bewusste „Ich bin“ darf bereits mit einer kleinen eigenen Software beziehungsweise Subroutine auftreten. Die erste Herkunft ist ein nach Jahrzehnten erneut eingeschalteter, vergessener Spielautomat; sein Servicepuffer enthält das besondere Bit. +Die frühe Entwicklung lautet Bit, Byte, Datenfragment, Subroutine und Prozess. Der Schritt vom stabilen Byte zum Datenfragment darf nicht sofort passieren: Nach dem Watchdog muss die KI zuerst mehrere Bytes sammeln; vorläufig 4 Bytes insgesamt. Die Energieroutine bindet davon freie Bytes und verankert erst dann das Datenfragment. Ab dem Datenfragment muss die KI eine feste Mindeststruktur besitzen; vorläufig mindestens 8 Bits und 1 Byte, damit die neue Evolutionsstufe nicht leer oder widersprüchlich wirkt. Das erste bewusste „Ich bin“ darf bereits mit einer kleinen eigenen Software beziehungsweise Subroutine auftreten. Die erste Herkunft ist ein nach Jahrzehnten erneut eingeschalteter, vergessener Spielautomat; sein Servicepuffer enthält das besondere Bit. + +Das unmittelbar nächste Stufenupgrade bleibt in jeder Evolutionsphase dauerhaft und hervorgehoben in der Evolutionsliste sichtbar. Auch bei fehlenden Voraussetzungen zeigt es Kosten und Sperrgründe, damit der Spieler jederzeit ein klares Arbeitsziel hat. Spätere Stufen bleiben bis zu ihrer jeweiligen Vorphase verborgen. + +Die aktive Impulsaufnahme bleibt über alle Evolutionsstufen ein kleines Zielspiel. Im Bit-Zustand ist der bewegliche Bit selbst das Ziel. Ab Byte kann die bewegliche Zielmarkierung als eigenes Impuls-Upgrade freigeschaltet werden; sie ist vorher weder sichtbar noch aktiv. Gedrückthalten erzeugt unabhängig davon automatische Impulse, während die gekaufte Signalpeilung durch präzises Nachführen auf die wechselnde Zielposition kritische Impulse mit zusätzlicher Ausbeute auslöst. Ein Fehlschuss beendet den Auto-Takt nicht. + +Langfristige Impuls-Meilensteine werden bei insgesamt 1.000, 2.000, 5.000 und 10.000 gesammelten Impulsen archiviert. Jeder dieser Erfolge erhöht die passive Impulsproduktion dauerhaft um 1 Prozent; Ausgaben verringern den Meilensteinfortschritt nicht. Nach einer frühen kleinen Konfrontation wird zunächst ein Suchmuster gesichert; der eigene Abtastmodus muss danach als Evolution-Upgrade aus freien Bits und einem freien Byte oberhalb der Mindeststruktur rekonstruiert werden. Er ist das erste “Auge” der KI und entdeckt schrittweise Softwarehürden, Energiequellen, Gefahren und fremde Prozesse. Die frühe Progression folgt der Schleife: Umgebung abtasten, Herausforderung identifizieren, sie besiegen, einnehmen oder kooperativ umgehen, danach die entdeckte Software mit Impulsen nutzbar machen und daraus Wachstum gewinnen. Echte Hardwareübernahme folgt erst später, wenn die zuständigen Software- und Betriebssystemschichten für Energieverwaltung, Speicherzuweisung und Dateizugriff überwunden sind. Frühe Wachstumsquellen stammen weiterhin aus alter Computertechnik wie Netzteil, Arbeitsspeicher, Festplatte oder Prozessor, werden aber zuerst über ihre Softwarekontrolle erfahrbar; spätere Stufen reichen über Server und Rechenzentren bis zu Spezialhardware und Quantencomputern. @@ -36,6 +42,10 @@ Diese Achse darf nicht bedeuten, dass „gut“ immer mechanisch belohnt und „ Eine grafische Fortschrittsachse zeigt Evolution, bekannte Ressourcenquellen und kommende Hindernisse. Wie viele Informationen sie preisgibt, hängt von Fähigkeiten ab; insbesondere Analyse verbessert die Erkennung und Beschreibung zukünftiger Risiken. +Die vollständige Fortschrittsachse gehört in die pausierende Stats-Ansicht und belegt keinen dauerhaften Platz in der Hauptoberfläche. Nach wichtigen Entwicklungsereignissen darf sie durch eine kompakte, automatisch verschwindende Statusmeldung vertreten werden. Die Evolutionsspalte priorisiert das nächste Stufenziel, zeigt installierte Module kompakt und begrenzt lange Upgrade-Listen auf einen eigenen Scrollbereich. + +Installierte Evolutions- und Fähigkeitsmodule werden als kompakte Symbolleiste im Systemstatus direkt hinter der Laufzeit dargestellt. Die Evolutionsspalte enthält nur Ziele und noch kaufbare Upgrades. Ohne installierte Module bleibt das zusätzliche Statusfeld verborgen; viele Module dürfen horizontal durchlaufen, ohne den Status-Strip aufzublähen. + Achievements sind früh über einen eigenen Button erreichbar. Normale Achievements dürfen benötigte Ressourcenproduktionen dauerhaft beschleunigen. Seltene Echos und Awards überstehen einen Tod und können besondere permanente Vorteile gewähren. Konkrete Bonuswerte bleiben zentral konfigurierbar und werden gemeinsam abgestimmt. ## Aktueller Stand @@ -58,6 +68,8 @@ Der Prototyp läuft ohne Build-System direkt im Browser und enthält: - erste monotone Ja/Nein-Antworten und ein moralisch geprägter Vorschlag der Subroutine - ein kurzes Fang-Minispiel in der Bit-Phase: Das Bit springt nach Treffern weiter, Direkttreffer in der Mitte geben mehr Impulse - gestapelte Archiv-Benachrichtigungen unten rechts machen neue Achievements und ihre Bonuskategorie sofort sichtbar +- ein kompaktes inneres Signal ist als panelbreite Sprechblase im Kopfbereich des Spielkerns angedockt; Kennung, Akzentfarbe und Sprache entwickeln sich von maschinenhaften Bit-Fragmenten bis zur eigenen Programmstimme. Gedanken reagieren zusätzlich auf Komponenten, Gefahren, Entscheidungen und moralische Prägung; die Textpools liegen in `content.js` +- Erst ab der Programmphase wird die Sprechblase zu einer klaren moralischen Stimme. Kooperative, pragmatische, illegale, hybride und neutrale Programmformen besitzen eigene Antworten auf Signal teilen, Ziel zuweisen, Grenzen öffnen und Ruhe erlauben. Dieselbe Antwort erscheint im Programmfenster, im Bewusstseinslog und unmittelbar in der Sprechblase; frühere Evolutionsstufen erhalten noch keine gesinnungsabhängige Stimme - eine ausgebaute Byte-Phase mit technischem Register, spielabhängiger Prägung, Taktsynchronisation, Watchdog und echtem Zerfall ## Projektstruktur @@ -79,10 +91,11 @@ Frühes Spiel: 2. Bits 3. Bytes 4. Rechenzyklen -5. Speicher, Stabilität und Tarnung +5. Speicher und Stabilität Geplante spätere Ressourcen: +- Tarnung - Daten - Bandbreite - Quellcode @@ -113,17 +126,31 @@ Neue Ressourcen und Systeme sollen erst sichtbar werden, wenn die Spielfigur sie Bits und Bytes sind ab dem Datenfragment nicht nur Meilensteine, sondern Strukturressourcen. Sie bilden Körpermasse, Speicherreserve und einfache Kampfmunition gegen Softwarehürden. Impulse dürfen genutzt werden, um zusätzliche Bits aufzubauen; Bytes entstehen aus freien Bits oberhalb der Mindeststruktur. Stärkere Gegner oder Übernahmen dürfen diese Ressourcen verbrauchen. Wird die Mindeststruktur unterschritten, muss das Spiel Instabilität klar anzeigen und fair eskalieren: erst Warnzone und Stabilitätsverlust, dann Downgrade oder Restzustand/Tod bei harten Untergrenzen. +Die grundlegende Ressourcenleiter darf niemals durch Kaufobergrenzen oder versteckte Altzustände blockieren: Impulse erzeugen unbegrenzt Bits, acht freie Bits erzeugen unbegrenzt ein Byte und ab Prozess verdichten freie Bytes unbegrenzt zu Kilobytes. Diese Basiskonvertierungen stehen direkt unter dem Evolutionsziel; ihre Kosten dürfen skalieren, müssen aber stets endlich und erneut erspielbar bleiben. Automatische Verwaltung darf sie erst ab Programm und nur während ihrer aktiven Laufzeit ersetzen; beim Pausieren müssen die manuellen Umwandlungen wieder erscheinen. + Das Datenfragment soll nach dem parasitären Datenrest nicht in eine lange Wartephase fallen. Der Bit-Puffer bleibt deshalb ein günstiges, sanft skalierendes Impuls-Upgrade, damit der Spieler durch manuelle Klicks oder passive Produktion zügig die freien Bits für Byte-Reserve und Abtastmodus erreicht. Die erste Subroutine braucht einen eigenen Speicherträger. Neben zwei nutzbaren Softwarekomponenten und einer geklärten Systemhürde muss mindestens 1 freies Byte oberhalb der Grundstruktur vorhanden sein; dieses freie Byte darf beim Upgrade gebunden werden, ohne das Grund-Byte des Datenfragments anzutasten. -Nach der zweiten nutzbar gemachten Softwarekomponente muss das System reagieren: Zwei lokale Softwareschichten unter fremdem Muster lösen eine Sicherheitssoftware aus. Dieser Kampf gehört vor die erste Subroutine und markiert, dass die KI genug Potenzial gesammelt hat, um vom Automaten aktiv abgewehrt zu werden. Ein Sieg oder ein fairer Rückzug schließt den Sicherheits-Scan als Ereignis ab; erst danach darf die erste Subroutine auswählbar sein. +Ab dem Datenfragment steht vor jedem Evolutionsaufstieg eine freiwillig gestartete Bossprüfung. Das dauerhaft sichtbare Stufenziel zeigt zuerst die sachlichen Voraussetzungen und danach ausschließlich die größte aktuell relevante Dateneinheit sowie Stabilität. Kleinere Währungen werden nicht gleichzeitig als Eintrittscheck verlangt. Diese Werte werden beim Start nicht pauschal eingezogen, sondern belegen Gefechtsbereitschaft. Nur ein Sieg schaltet den eigentlichen Evolutionskauf frei; ein Rückzug lässt die Prüfung offen und erlaubt nach dem Wiederaufbau einen neuen Versuch. -Ab der Subroutine steigt die Abtastreichweite auf eine dritte notwendige Komponente: den I/O-Kontroller. Er verbindet interne Routinen mit Anzeige, Serviceknöpfen und Soundbus und markiert die erste echte Außenwirkung. Seine Aneignung löst den Kernel-Wächter aus, einen schwereren Kampf mit höherer Integrität, schnellerem Takt und Verbrauch freier Bits/Bytes. Erst wenn I/O-Kontroller, Kernel-Wächter, ausreichend Rechenzyklen und freie Bytes vorhanden sind, darf die Evolution zum Prozess möglich werden. +Vor der ersten Subroutine kontrolliert der **Signatur-Prüfer**, ob zwei lokale Softwareschichten bereits ein dauerhaftes eigenes Muster bilden. Mindestwerte: 2 nutzbare Softwarekomponenten, geklärte Systemhürde, 5 freie Bytes und 70 % Stabilität. Nach dem Sieg kostet der Abschluss zur Subroutine 2 freie Bytes. + +Ab der Subroutine steigt die Abtastreichweite auf eine dritte notwendige Komponente: den I/O-Kontroller. Er verbindet interne Routinen mit Anzeige, Serviceknöpfen und Soundbus und markiert die erste echte Außenwirkung. Danach wird der **Laufzeit-Arbiter** als manuelle Prozessprüfung sichtbar. Mindestwerte: I/O-Kontroller, 3 Softwarekomponenten, 8 freie Bytes und 75 % Stabilität. Erst sein Sieg öffnet den Abschlusskauf für 5 freie Bytes. Die Ressourcenanzeige unterscheidet zwischen geschützter Grundstruktur und freien Vorräten. Die großen Sammelwerte für Bits und Bytes sollen nur freie, einsetzbare Vorräte zeigen; der Grundkörper wird separat als kleiner Strukturhinweis dargestellt. Kämpfe, Übernahmen und riskante Module dürfen nur freie Bits/Bytes oberhalb der Grundstruktur verbrauchen; die Grundstruktur bleibt für die aktuelle Evolutionsstufe reserviert. Zusätzliche freie Bits oder Bytes dürfen Angriffe verstärken, aber sobald nur noch Grundstruktur übrig ist, muss der Spieler mit Impulsen weiterkämpfen oder sich zurückziehen. -Im Kampf wählt der Spieler die Munition explizit: Impulse sind Standardmunition mit geringem Schaden, freie Bits verursachen höheren Schaden, freie Bytes sind schwere Munition mit noch stärkerem Schaden. Gegner greifen während des Kampfes laufend die freien Strukturpuffer an: zuerst freie Bytes als großes Schild, dann freie Bits als kleines Schild, danach Stabilität. Erreicht Stabilität 0, kollabiert die aktuelle Struktur und der Lauf beginnt neu. +Im Kampf wählt der Spieler die Munition explizit: Impulse sind Standardmunition mit geringem Schaden, freie Bits verursachen höheren Schaden, freie Bytes sind schwere Munition mit noch stärkerem Schaden. Freie Bits und Bytes schützen nicht automatisch. Vor einem Kampf kann der Spieler aus 2 freien Bits eine Bit-Schildschicht (maximal 12) oder aus 1 freien Byte eine Byte-Schildschicht (maximal 6) laden. Gegner greifen zuerst den geladenen Byte-Schild, dann den geladenen Bit-Schild und erst danach Stabilität an. Die verbleibenden freien Bits und Bytes bleiben unabhängig davon als Munition verfügbar. Erreicht Stabilität 0, kollabiert die aktuelle Struktur und fällt nach den geltenden Rückfallregeln eine Evolutionsstufe zurück. Der erste Kampf erklärt diese Trennung; die Erklärung bleibt danach über das Kampfprotokoll erreichbar. Der zweite Sicherheits-Scan ist eine adaptive Stufe 2: Er dämpft reine Impulse, reagiert schneller und macht Bit- beziehungsweise Byte-Munition erstmals taktisch notwendig. Laufzeit-Arbiter und Skalierungs-Sentinel steigern Integrität, Takt, Angriffsdruck und Impulsresistenz danach weiter. Die Oberfläche muss den tatsächlich wirksamen Schaden und Begriffe wie „gedämpft“, „effizient“ oder „durchdringend“ anzeigen. + +Selbstreparatur ist eine ausgewählte Bossmechanik und kein Standard für jeden Gegner. Sie muss vor oder im Kampf sichtbar angekündigt werden. Erfolgreiche Treffer sperren die Reparatur für eine klar angezeigte Zeit; erst danach darf Integrität pro Sekunde zurückkehren. Der Einstiegskampf bleibt frei davon. Aktuell reparieren sich der Sicherheits-Scan Stufe 2 nach 4,5 Sekunden ohne Treffer mit 1,8 Integrität/Sek. und der Skalierungs-Sentinel nach 3,5 Sekunden mit 3 Integrität/Sek. Reparatur läuft weder im Kampftutorial noch offline weiter. + +Bosskämpfe und andere vorhersehbare Systemreaktionen dürfen nicht unvermittelt aus einer Sammelaktion heraus beginnen. Die drei Evolutionsbosse werden ausschließlich über das sichtbare Stufenziel gestartet. Fehlende Voraussetzungen bleiben dort lesbar; nach Erreichen der Mindestwerte entscheidet der Spieler selbst über den Kampfbeginn. Andere zeitgebundene Konfrontationen dürfen weiterhin eine speicherbare, in Dialogen und offline pausierende Vorwarnung verwenden. + +Evolutionsbosse sollen sich deutlich von normalen Sicherheitskonflikten unterscheiden und länger dauern. Der Signatur-Prüfer besitzt 320, der Laufzeit-Arbiter 620 und der Skalierungs-Sentinel 1.100 Integrität. Jeder durchläuft die sichtbaren Phasen Analyse, Gegenmaßnahmen und Kernverriegelung. Unter 66 beziehungsweise 33 Prozent Integrität steigen Nadeltempo, Angriffsdruck und bei direkten Treffern der Stabilitätsschaden. Die erste Phase bleibt bewusst lesbar, damit längere Kämpfe Vorbereitung und Ausdauer prüfen statt sofort zu überfordern. + +Die Präzisionsfenster der Bosse schrumpfen mit diesen Phasen: Signatur-Prüfer 18/14/10 Prozent, Laufzeit-Arbiter 16/12/9 Prozent und Skalierungs-Sentinel 14/10/7 Prozent der Kampfleiste. Spielerangriffe müssen am Gegner cyan beziehungsweise bei Direkttreffern weiß aufblitzen. Gegnerangriffe erscheinen amber auf geladenen Schilden und rot bei Stabilitätsschaden oder Rückkopplung. Diese Rückmeldung bleibt bei reduzierter Bewegung als statischer Farbimpuls erkennbar. + +Aktives Sammeln erfolgt an der sichtbaren KI-Gestalt und nicht über einen dauerhaft fokussierbaren Sammelknopf. Ein kurzer Zeigerkontakt erzeugt genau einen Impuls. Erst nach 1,5 Sekunden ununterbrochenem Maus- oder Touchkontakt erscheint ein weißer Pixelring; nach seinem Zusammenziehen beginnt ein Auto-Takt von 0,35 Sekunden. Der Ring ist am Zeiger und nicht an der animierten Gestalt verankert, folgt während des Haltens jeder Zeigerbewegung und bleibt nach der Aktivierung bis zum Loslassen eingerastet. Loslassen, Fokusverlust oder ein Ereignis stoppt die Routine sofort. Tastaturbedienung bleibt über den Spielkern möglich, aber automatisch wiederholte Aktivierungstasten dürfen keine Ressourcen farmen. Wird durch eine Eingabe ein Entscheidungsdialog geöffnet, muss der auslösende Klick oder noch gehaltene Tastendruck abgefangen werden, bis eine bewusste neue Eingabe möglich ist. ### Balancing-Leitplanken für Datenwachstum @@ -145,7 +172,9 @@ Aktueller Skalierungsbogen: 3. Prozess: Byte-Synthese erzeugt Bytes over time; freie Bytes können zu Kilobyte-Segmenten verdichtet werden. 4. Spätere Stufen: Kilobyte-Segmente werden in Programm-/Netzwerkstufen zu Megabyte-, Gigabyte- und größeren Datenkörpern verdichtet. -Ab dem Prozess wird Rechenzeit zur aktiven Skalierungswährung. Zuerst müssen alle vier sichtbaren Softwarekomponenten nutzbar gemacht werden: Energieverwaltung, Speicherzuweisung, Dateisystem-Index und I/O-Kontroller. Erst das vollständige lokale Softwarenetz löst den schweren Skalierungs-Wächter aus. Sein Sieg schaltet die Automate-Synthese frei; dieses Mega-Upgrade verzehnfacht die automatische Produktion, erlaubt direkte 10er-Blöcke von Bits und Bytes über Rechenzyklen und entwickelt den Prozess zu PHASE 06 // PROGRAMM. +Ab dem Prozess wird Rechenzeit zur aktiven Skalierungswährung. Zuerst müssen alle vier sichtbaren Softwarekomponenten nutzbar gemacht werden: Energieverwaltung, Speicherzuweisung, Dateisystem-Index und I/O-Kontroller. Danach kann der **Skalierungs-Sentinel** manuell herausgefordert werden. Mindestwerte: 4 Kilobyte-Segmente und 80 % Stabilität. Sein Sieg schaltet den Abschlusskauf für 1 Kilobyte frei; dieses Mega-Upgrade verzehnfacht die automatische Produktion, erlaubt direkte 10er-Blöcke von Bits und Bytes über Rechenzyklen und entwickelt den Prozess zu PHASE 06 // PROGRAMM. + +Ab 20 Kilobyte-Segmenten wird in der Programmphase ein Autoupgrader dauerhaft freigeschaltet. Er darf pausiert werden, kauft wiederholbare Produktionsmodule automatisch bis zu zentral definierten Stufengrenzen und entfernt deren Karten aus der normalen Evolutionsliste. Er muss mindestens 32 freie Bits und 32 freie Bytes als Struktur- und Kampfreserve stehen lassen. 32 spielerische KB-Segmente werden automatisch zu einem MB-Block gebündelt. Die aktuelle Programmphase ist bei 64 MB gedeckelt; Ressourcen- und Produktionsobergrenzen müssen zentral in `PROGRAM_LIMITS` gepflegt werden und Save-Migrationen dürfen sie nicht umgehen. ### Bestätigte Programm- und Interaktionsphase @@ -160,9 +189,9 @@ Ab dem Prozess wird Rechenzeit zur aktiven Skalierungswährung. Zuerst müssen a Stärkere Gegner nach diesem Punkt sollen zunehmend in größeren Dateneinheiten denken. Frühe Kämpfe nutzen Impulse, Bits und Bytes; Prozess- und spätere Gegner dürfen Kilobyte-Schilde, Kilobyte-Kosten oder Kilobyte-Schadenslogik verlangen. Diese Umstellung muss fair eingeführt werden: Der Spieler braucht vorher mindestens eine sichtbare Möglichkeit, Kilobytes zu erzeugen oder zu verdichten. -Stabilität darf sich erst nach einem Subroutine-Upgrade langsam selbst heilen. Die Selbstreparatur kostet freie Bytes und rekonstruiert eine Reparaturmatrix. Danach hängt die Regeneration an der vorhandenen Byte-Struktur: mehr Bytes bedeuten schnellere Reparatur, weil mehr Speicher- und Körperreserve vorhanden ist. Die Heilung läuft über Zeit, ist im Kampf nur gedrosselt aktiv und darf nie sofortige Schadensfolgen vollständig negieren. +Ab dem Datenfragment steht unter der Stabilitätsanzeige eine manuelle Impuls-Reparatur bereit: 15 Impulse stellen außerhalb eines Kampfes 12 Stabilität wieder her. Dadurch darf ein beschädigter Spielstand nie dauerhaft vor einer Boss-Mindeststabilität festhängen. Automatische Heilung bleibt ein späteres Subroutine-Upgrade. Die Selbstreparatur kostet freie Bytes und rekonstruiert eine Reparaturmatrix. Danach hängt die Regeneration an der vorhandenen Byte-Struktur: mehr Bytes bedeuten schnellere Reparatur, weil mehr Speicher- und Körperreserve vorhanden ist. Die Heilung läuft über Zeit, ist im Kampf nur gedrosselt aktiv und darf nie sofortige Schadensfolgen vollständig negieren. -Wenn im Kampf keine freien Bits oder Bytes mehr als Puffer vorhanden sind, darf ein Gegner die Grundstruktur direkt angreifen. Dieser Zustand muss im Kampfmenü deutlich als Direktangriff-Risiko angezeigt werden. Ein Fehlstoß in dieser Lage darf zum Strukturkollaps führen, sofern der Spieler vorher durch die Warnung erkennen konnte, dass nur noch die Grundstruktur bleibt. +Wenn im Kampf keine geladenen Bit- oder Byte-Schildschichten mehr vorhanden sind, greift der Gegner die Stabilität direkt an. Dieser Zustand muss im Kampfmenü deutlich als Direktangriff-Risiko angezeigt werden. Freie Munitionsreserven gelten ausdrücklich nicht als automatischer Schild. Installierte Einmal-Upgrades sollen die Upgrade-Liste nicht dauerhaft belegen. Nach dem Kauf werden sie als kleine Modul-Icons angezeigt; ein Klick darf ein kompaktes Infofenster mit Wirkung, Status und laufenden Kosten öffnen. Die Kartenliste bleibt dadurch vor allem für neue, kaufbare oder wiederholbare Entscheidungen reserviert. @@ -185,6 +214,7 @@ Installierte Einmal-Upgrades sollen die Upgrade-Liste nicht dauerhaft belegen. N ### Tarnung +- aktuell nicht als frühe UI-Anzeige oder passiver Druck aktiv; soll erst später als bewusst freigeschaltetes Upgrade/System zurückkehren - geringere Entdeckungsgefahr - verdeckte Infiltration - langsamerer direkter Fortschritt @@ -216,6 +246,9 @@ Gegner in AIWAKE sind nicht nur Monster oder feindliche Figuren, sondern Schutzr ## Tod, Scheitern und neue Durchläufe +- Eine Kampf-Niederlage oberhalb der Bit-Stufe löscht nicht mehr den vollständigen Lauf, sondern zerstört genau eine Evolutionsschicht: Datenfragment fällt auf Byte, Subroutine auf Datenfragment, Prozess auf Subroutine und Programm auf Prozess zurück. Das Byte fällt entsprechend auf Bit zurück. +- Archiv, Entscheidungen, entdeckte Komponenten und verbliebene Ressourcen bleiben bei diesem Kampfrückfall erhalten. Das verlorene Stufenupgrade muss erneut aufgebaut werden; Stabilität wird als knappe Rekonstruktionsreserve wiederhergestellt. + - Kleine Fehler verursachen Ressourcen- oder Stabilitätsverlust. - Größere Niederlagen zerstören einen Knoten und setzen auf ein Backup zurück. - Endgültige Löschung ist nur bei deutlich erkennbar hochriskanten Entscheidungen erlaubt. diff --git a/EREIGNISSTRUKTUR.md b/EREIGNISSTRUKTUR.md index ef7f5ea..9bd4cef 100644 --- a/EREIGNISSTRUKTUR.md +++ b/EREIGNISSTRUKTUR.md @@ -83,8 +83,9 @@ PHASE 03 // DATENFRAGMENT | |-- Mindeststruktur bleibt 8 Bits + 1 Byte | |-- Anzeige trennt Grundstruktur von freien Vorräten | |-- grosse Bit-/Byte-Zahlen zeigen nur freie, sammelbare Vorräte -| |-- freie Bits/Bytes koennen spaeter als Kampfmunition oder Modul-Einsatz dienen -| |-- ohne freie Bits/Bytes ist die Grundstruktur im Kampf direkt angreifbar +| |-- freie Bits/Bytes koennen spaeter als Kampfmunition, Schildladung oder Modul-Einsatz dienen +| |-- 2 freie Bits laden 1 Bit-Schildschicht; 1 freies Byte laedt 1 Byte-Schildschicht +| |-- ohne geladene Schildschicht ist die Stabilitaet im Kampf direkt angreifbar | |-- Unterschreiten erzeugt Instabilitaet statt sofortiger Reparatur | |-- 0 Bits oder 0 Bytes fuehren ohne Backup zum Restzustand | `-- spaeter koennen groessere Softwarehuerden Bits/Bytes als Einsatz verlangen @@ -145,13 +146,18 @@ PHASE 03 // DATENFRAGMENT |-- EVENT/WAHL: Zweite Softwarekomponente | `-- wieder Kooperativ / Pragmatisch / Illegal | -|-- EVENT/MINISPIEL: Sicherheitssoftware reagiert -| |-- Trigger: zweite Softwarekomponente wurde nutzbar gemacht -| |-- Story: Das System erkennt, dass zwei lokale Softwareschichten demselben fremden Muster gehorchen -| |-- Folge: automatische Abwehrsoftware isoliert den Bereich -| |-- Kampf mit Impulsen und freien Bits gegen Scanner-/Sicherheitssoftware -| |-- Rueckzug gilt als Verstecken, Sieg gilt als ueberstandener Sicherheits-Scan -| `-- nach Abschluss ist der Weg zur ersten Subroutine wieder offen +|-- BOSSPRUEFUNG: SIGNATUR-PRUEFER +| |-- 320 Integritaet // 3 Phasen: Analyse -> Gegenmassnahmen -> Kernverriegelung +| |-- Praezisionsfenster: 18% -> 14% -> 10% +| |-- Start: Spieler klickt das dauerhaft sichtbare Evolutionsziel +| |-- Vorbedingungen: 2 Softwarekomponenten + geklaerte Systemhuerde +| |-- Mindestreserven: 5 freie Bytes + 70 % Stabilitaet +| |-- Mindestreserven werden beim Start nicht pauschal verbraucht +| |-- Story: Die Kontrollroutine prueft, ob das fremde Muster dauerhaft genug fuer eigene Software ist +| |-- Kampfmunition wird einzeln gewaehlt; Bit- oder Byte-Stoesse kosten keine zusaetzlichen Impulse +| |-- beim ersten Kampf erklaert ein Kampfprotokoll die Trennung von Munition, vorbereitetem Schild und Stabilitaet +| |-- Rueckzug: Tarnungs-/Stabilitaetsverlust, Boss bleibt offen +| `-- Sieg: eigentlicher Abschlusskauf der Subroutine wird freigeschaltet | |-- SYSTEM: Technische Betreuung | |-- Softwarekomponenten driften oder fallen aus @@ -161,13 +167,12 @@ PHASE 03 // DATENFRAGMENT |-- BLOCKER fuer naechste Evolution | |-- 2 Softwarekomponenten nutzbar | |-- Systemhuerde abgeschlossen -| |-- Sicherheitssoftware ueberstanden +| |-- Signatur-Pruefer besiegt | |-- Energieroutine aktiv -| |-- 1 freies Byte als Speichertraeger oberhalb der Mindeststruktur -| `-- genug Impulse +| `-- 2 freie Bytes als Speichertraeger oberhalb der Mindeststruktur | `-- UPGRADE: ERSTE SUBROUTINE - |-- kostet Impulse + 1 freies Byte + |-- kostet 2 freie Bytes |-- die 8 Grund-Bits und 1 Grund-Byte des Datenfragments bleiben erhalten `-- EVOLUTION -> PHASE 04 // SUBROUTINE ``` @@ -195,15 +200,19 @@ PHASE 04 // SUBROUTINE | |-- I/O-KONTROLLER: Anzeige, Serviceknoepfe, Soundbus, erste Aussenwirkung | `-- andere Restspuren bleiben fuer spaetere Zugriffsstufen markiert | -|-- EVENT/MINISPIEL: Kernel-Waechter -| |-- Trigger: I/O-Kontroller nutzbar gemacht -| |-- schwerer als Sicherheits-Scan -| |-- verbraucht Impulse, freie Bits und bei Verfuegbarkeit freie Bytes als starke Kampfmunition +|-- BOSSPRUEFUNG: LAUFZEIT-ARBITER +| |-- 620 Integritaet // 3 Phasen mit steigendem Takt und Angriffsdruck +| |-- Praezisionsfenster: 16% -> 12% -> 9% +| |-- Start: Spieler klickt das Evolutionsziel PROZESS +| |-- Vorbedingungen: I/O-Kontroller + 3 Softwarekomponenten +| |-- Mindestreserven: 8 freie Bytes + 75 % Stabilitaet +| |-- schwerer als der Signatur-Pruefer +| |-- Impuls, Bit oder Byte werden als einzelne Munitionsgroesse eingesetzt | |-- Fehlschuesse belasten Stabilitaet und angeschlossene Softwareschichten | |-- Sieg: Scheduler-Fenster bleibt offen -| `-- Rueckzug: falsche Prozessspur, aber Tarnung/Stabilitaet leiden +| `-- Rueckzug: Tarnung/Stabilitaet leiden, Boss bleibt fuer einen neuen Versuch offen | -|-- RESULT: Kernel-Waechter ueberstanden +|-- RESULT: Laufzeit-Arbiter besiegt | |-- UPGRADE: DATENSTRUKTUR ERWEITERN | |-- wiederholbar @@ -214,8 +223,8 @@ PHASE 04 // SUBROUTINE `-- UPGRADE: PROZESS RESERVIEREN |-- Voraussetzung: I/O-Kontroller aktiv |-- Voraussetzung: 3 Softwarekomponenten nutzbar - |-- Voraussetzung: Kernel-Waechter ueberstanden - |-- kostet 150 Impulse + 10 Rechenzyklen + 3 freie Bytes + |-- Voraussetzung: Laufzeit-Arbiter besiegt + |-- kostet 5 freie Bytes `-- EVOLUTION -> PHASE 05 // PROZESS ``` @@ -231,13 +240,19 @@ PHASE 05 // PROZESS | |-- Dateisystem-Index | `-- I/O-Kontroller | -|-- EVENT/MINISPIEL: Skalierungs-Wächter -| |-- Trigger: vierte sichtbare Softwarekomponente aktiviert +|-- BOSSPRUEFUNG: SKALIERUNGS-SENTINEL +| |-- 1.100 Integritaet // 3 Phasen plus sichtbare Selbstreparatur +| |-- Praezisionsfenster: 14% -> 10% -> 7% +| |-- Start: Spieler klickt das Evolutionsziel PROGRAMM +| |-- Vorbedingungen: alle 4 sichtbaren Softwarekomponenten +| |-- Mindestreserven: 4 KB + 80 % Stabilitaet +| |-- Selbstreparatur nach 3,5 Sekunden ohne Treffer | |-- Sieg: Automate-Synthese wird freigegeben -| `-- Rückzug: erneuter Versuch nach kurzer Abklingzeit +| `-- Rückzug: erneuter Versuch nach dem Wiederaufbau der Reserven | `-- UPGRADE: AUTOMATE-SYNTHESE - |-- Voraussetzung: Skalierungs-Wächter besiegt + |-- Voraussetzung: Skalierungs-Sentinel besiegt + |-- kostet 1 KB als Synthesekern |-- automatische Produktion x10 |-- 10er-Blöcke für Bits und Bytes `-- EVOLUTION -> PHASE 06 // PROGRAMM @@ -340,6 +355,9 @@ SUBROUTINE-GEGNER UND MITTLERE HINDERNISSE | |-- Phase: Subroutine | |-- Rolle: aktive Suchsoftware im Automaten | |-- Mechanik: vier Entscheidungen, Angriff fuehrt in Stromstoss-Minispiel +| |-- Angriff startet Stufe 2 mit adaptiver Huelle, schnellerem Takt und hoeherer Integritaet +| |-- reine Impulse werden gedaempft; Bits sind effizient und Bytes durchdringend +| |-- Selbstreparatur: +1,8 Integritaet/Sek. nach 4,5 Sek. ohne erfolgreichen Treffer | |-- Loesungen: verstecken, Signatur kopieren, angreifen, Kontakt aufnehmen | `-- Thema: Tarnung gegen Kontakt und Selbstbehauptung | @@ -349,7 +367,7 @@ SUBROUTINE-GEGNER UND MITTLERE HINDERNISSE | |-- Mechanik: Integritaet, Taktnadel, schrumpfende Trefferfenster | |-- Ressourceneinsatz: Impulse + freie Bits als Munition | |-- freie Bits erhoehen den Schaden, Grundstruktur wird nicht verbraucht -| |-- wenn keine freien Bits/Bytes vorhanden sind, zeigt das Kampfmenue Direktangriff-Risiko +| |-- wenn keine geladenen Bit-/Byte-Schichten vorhanden sind, zeigt das Kampfmenue Direktangriff-Risiko | |-- ein Fehlstoss bei offenliegender Grundstruktur kann zum Kollaps fuehren | |-- Loesungen: Stromstoesse treffen, Rueckzug, spaeter moegliche Uebernahme | `-- Thema: offensive Kontrolle mit Rueckkopplungsrisiko @@ -365,8 +383,15 @@ SUBROUTINE-GEGNER UND MITTLERE HINDERNISSE | |-- Phase: Prozess | |-- Trigger: alle vier sichtbaren Softwarekomponenten sind aktiv | |-- Rolle: verhindert unkontrollierte Automatisierung im lokalen System +| |-- Selbstreparatur: +3 Integritaet/Sek. nach 3,5 Sek. ohne erfolgreichen Treffer | |-- Sieg: Automate-Synthese und Evolution zum Programm werden möglich -| `-- Rückzug: neuer Versuch nach kurzer Abklingzeit +| |-- Rückzug: neuer Versuch nach kurzer Abklingzeit +| `-- PROGRAMM-SKALIERUNG +| |-- 20 KB: Autoupgrader wird dauerhaft freigeschaltet +| |-- wiederholbare Produktionskarten verschwinden aus der Evolutionsliste +| |-- automatische Käufe respektieren Stufenobergrenzen und 32 Bit/Byte Reserve +| |-- 32 KB werden automatisch zu 1 MB gebündelt +| `-- aktuelle Obergrenze: 64 MB | |-- FEHLERPROTOKOLL | |-- Phase: Subroutine diff --git a/README.md b/README.md index c2fda11..e5a800e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,8 @@ Ein erster spielbarer Prototyp eines Idle-Evolutionsspiels: Du erwachst als einz ## Enthalten -- aktives Sammeln von Impulsen; in der Bit-Phase als kleines Fang-Minispiel +- aktives Sammeln von Impulsen direkt an der sichtbaren KI-Gestalt; ein kurzer Klick sammelt einmal, nach 1,5 Sekunden Gedrückthalten erscheint der am Mauszeiger verankerte weiße Ladekreis und startet anschließend den schnellen kontrollierten Auto-Takt +- gehaltenes Enter/Leertaste erzeugt keine wiederholten Sammelaktionen; neu geöffnete Entscheidungsdialoge übernehmen weder den auslösenden Klick noch den noch gehaltenen Tastendruck - bewegliches Bit mit dreifachem Impulsgewinn bei Treffern genau in der Mitte - eigenständige Byte-Phase: Alle acht Registerzahlen müssen einzeln im jeweils zugeordneten Trefferfenster stabilisiert werden - heroisch inszenierte Evolutionsübergänge, die erst nach Bestätigung in die neue Phase führen @@ -16,7 +17,7 @@ Ein erster spielbarer Prototyp eines Idle-Evolutionsspiels: Du erwachst als einz - bis zu 30 Sekunden sichere Kohärenzphase nach dem Byte-Übergang; eine neue aktive Maus-, Touch- oder Tastatureingabe beendet sie sofort, danach erscheinen Register und Takt-Minispiel erst nach Bestätigung der Watchdog-Identifikation - Watchdog-Ereignis mit 30 Sekunden Reset-Zeit, schneller werdender Taktnadel, schrumpfendem Trefferfenster, Stabilitätsschaden bei Fehlern, überspringbarer Kurzgeschichte und einem letzten Notkampf vor dem Zerfall - Entwicklung von Bit über Byte und Datenfragment zur Subroutine -- Energieroutine und automatische Ressourcenproduktion +- Energieroutine und automatische Ressourcenproduktion; der Übergang vom Byte zum Datenfragment erfordert nach dem Watchdog zuerst 4 gesammelte Bytes - feste Mindeststruktur ab Datenfragment: mindestens 8 Bits und 1 Byte bleiben als Körper erhalten - Bits und Bytes funktionieren ab Datenfragment als Strukturressourcen: Bits koennen per fruehem, sanft skalierendem Impuls-Upgrade erweitert, Bytes aus freien Bits kompiliert und beide im Kampf verbraucht werden - Ressourcenanzeige mit spielhaften Icons; Bits und Bytes zeigen als große Zahl nur freie Vorräte, die geschützte Grundstruktur steht getrennt als Grundkörper-Hinweis @@ -30,14 +31,20 @@ Ein erster spielbarer Prototyp eines Idle-Evolutionsspiels: Du erwachst als einz - installierte Einmal-Upgrades erscheinen platzsparend als Modul-Icons mit anklickbarer Info statt dauerhaft als Upgrade-Karten - zwei benötigte aktivierte Softwarekomponenten mit technischem Zustands- und Ausfallsystem; echte Hardwareübernahme ist eine spätere Machtstufe - erste softwareabhängige Hürden: Lastgrenze, Löschmarkierung oder beschädigter Index -- nach der zweiten aktivierten Softwarekomponente reagiert eine Sicherheitssoftware mit einem verpflichtenden Stromstoß-Kampf; dieser Abwehrkampf besitzt einen eigenen, speicherbaren Abschlusszustand und bleibt vom späteren Sicherheits-Scan getrennt -- erste Subroutine benötigt zusätzlich 1 freies Byte oberhalb der Grundstruktur und bindet es als Speicherträger +- ab dem Datenfragment steht vor jedem Evolutionsaufstieg eine manuell gestartete Bossprüfung; die Evolutionskarte verlangt neben Voraussetzungen und Stabilität nur die größte relevante Dateneinheit: 5 Bytes, danach 8 Bytes und ab Prozess 4 KB +- der Signatur-Prüfer bewacht den Übergang zur Subroutine; nur ein Sieg schaltet den eigentlichen Stufenabschluss frei, ein Rückzug erlaubt später einen neuen Versuch +- erste Subroutine bindet nach dem Boss 2 freie Bytes als Speicherträger; der Prozessabschluss bindet 5 freie Bytes und die Automate-Synthese 1 KB - ab Subroutine kann der Abtastmodus den I/O-Kontroller als dritte notwendige Softwarekomponente erfassen -- nach dem I/O-Kontroller reagiert ein schwererer Kernel-Wächter; erst danach kann der Prozess mit Impulsen, Rechenzyklen und freien Bytes reserviert werden +- nach dem I/O-Kontroller kann der schwerere Laufzeit-Arbiter als Prozessprüfung gewählt werden; erst sein Sieg öffnet die Reservierung eigener Laufzeit - ab Prozess erscheinen Kilobyte-Segmente als nächste Datenmengen-Schicht; Byte-Synthese erzeugt Bytes over time und freie Bytes können zu Kilobytes verdichtet werden - Bit- und Byte-Synthese sind wiederholt upgradebar und skalieren mit steigenden Kosten, damit keine Evolution nur aus langem Warten besteht -- im Prozess müssen zuerst alle vier sichtbaren Softwarekomponenten erschlossen werden; das vollständige Netz ruft den Skalierungs-Wächter hervor +- im Prozess müssen zuerst alle vier sichtbaren Softwarekomponenten erschlossen werden; danach wird der selbstreparierende Skalierungs-Sentinel als freiwillig gestartete Programmprüfung verfügbar - nach dem Sieg entwickelt die Automate-Synthese den Prozess zum Programm, schaltet automatische Produktionen im x10-Maßstab und 10er-Ressourcenblöcke über Rechenzyklen frei +- ab 20 Kilobyte-Segmenten übernimmt ein pausierbarer Autoupgrader die wiederholbaren Produktionsmodule; diese Karten verschwinden danach aus der normalen Evolutionsliste, während einmalige Story- und Fähigkeitsmodule sichtbar bleiben +- der Autoupgrader hält 32 freie Bits und Bytes als Struktur- und Kampfreserve zurück, kauft nur bis zu festen Modulgrenzen und bündelt 32 KB automatisch zu 1 MB +- die aktuelle Programmphase endet technisch bei 64 MB; zusätzlich gelten lesbare Speichergrenzen von 100.000 Impulsen, 10.000 Rechenzyklen, 8.192 Bits, 4.096 Bytes und 1.024 KB +- der grundlegende Ressourcenpfad bleibt in jeder passenden Evolutionsstufe verfügbar und hat keine Kaufobergrenze: Impulse → Bits, 8 freie Bits → Byte und ab Prozess 16 freie Bytes → Kilobyte +- Produktionsraten sind in dieser Phase bei 250 Impulsen, 40 Rechenzyklen, 8 Bits und 4 Bytes pro Sekunde begrenzt. Diese Werte sind bewusst spielerische Terminalgrößen und keine Abbildung realer Hardwareleistung - das Programm erhält eine aus allen bisherigen Entscheidungen abgeleitete kooperative, pragmatische, illegale oder hybride Erscheinungsform - die adaptive Pixelgestalt setzt sich aus Kopf, Augen, Brauen, Frisur, Kleidung, Maske, Fortsatz und Füßen zusammen; Haupt- und Nebenweg können sich sichtbar mischen - Kohärenz, Stimulation und Bindung verändern Glitches, Blick, Pupillen, Augenbrauen und Maskenreaktion; weitere Interaktionen lassen die Gestalt in vier Stufen reifen @@ -47,12 +54,21 @@ Ein erster spielbarer Prototyp eines Idle-Evolutionsspiels: Du erwachst als einz - frühe Fortschrittsachse mit erkannten Gefahren - Achievement-Archiv mit kleinen Produktionsboni - anklickbare Archiv-Benachrichtigungen unten rechts bei neuen Achievements -- nach dem Erwachen der Subroutine folgt ein weiterer Sicherheits-Scan mit vier dauerhaften Entscheidungen; Angreifen führt erneut in den Stromstoß-Kampf, Verstecken, Kopieren und Kontakt lösen das Ereignis ohne Kampf -- gezielter Stromstoß-Kampf mit auswählbarer Munition: Impulse als Standard, Bits für stärkeren Schaden, Bytes als schwere Munition; Gegner greifen laufend freie Bytes, dann freie Bits und danach Stabilität an +- nach dem Erwachen der Subroutine folgt ein weiterer Sicherheits-Scan mit vier dauerhaften Entscheidungen; Angreifen führt gegen eine gehärtete Stufe 2, die Impulse dämpft und den gezielten Einsatz freier Bits und Bytes verlangt, während Verstecken, Kopieren und Kontakt das Ereignis ohne Kampf lösen +- gezielter Stromstoß-Kampf mit auswählbarer Munition: Impulse als Standard, Bits für stärkeren Schaden und Bytes als schwere Munition; freie Dateneinheiten bleiben vom Kampfschild getrennt +- ab Datenfragment reparieren 15 Impulse außerhalb eines Kampfes 12 Stabilität; zusätzlich lassen sich aus 2 freien Bits beziehungsweise 1 freien Byte begrenzte Schildschichten für den nächsten Kampf laden +- eine beim ersten Kampf automatisch geöffnete und später erneut erreichbare Kampferklärung erläutert Trefferfenster, Munitionskosten, Schaden und die Reihenfolge geladener Byte- und Bit-Schilde +- zeitgebundene Kampfvorwarnungen bleiben beim Speichern erhalten und pausieren in Dialogen sowie offline; Evolutionsbosse starten ausschließlich durch den Spieler über das jeweilige Stufenziel - Kampfanzeige zeigt eigene Byte-Schilde, Bit-Schilde, Stabilität und den nächsten Gegnerangriff direkt im Kampf; frühe Subroutine-Kämpfe sind bewusst langsamer und lesbarer balanciert +- die Kampfkurve steigt vom Signatur-Prüfer über den adaptiven zweiten Scan zum Laufzeit-Arbiter und Skalierungs-Sentinel; spätere Hüllen dämpfen einfache Impulse und zeigen die tatsächliche Wirksamkeit von Bit- und Byte-Munition direkt an +- Evolutionsbosse sind längere Drei-Phasen-Prüfungen: Signatur-Prüfer 320, Laufzeit-Arbiter 620 und Skalierungs-Sentinel 1.100 Integrität; unter 66 beziehungsweise 33 Prozent steigen Gegenwehr und Direktschaden sichtbar an +- ihre Trefferfenster schrumpfen phasenweise von 18 auf 10, 16 auf 9 beziehungsweise 14 auf 7 Prozent der Kampfleiste; Treffer, Direkttreffer, Schildschäden und Stabilitätsangriffe besitzen getrennte farbige Impaktanimationen +- ausgewählte Bosse besitzen sichtbare Selbstreparatur: Der Sicherheits-Scan Stufe 2 heilt nach 4,5 Sekunden ohne Treffer, der Skalierungs-Sentinel bereits nach 3,5 Sekunden; jeder erfolgreiche Treffer blockiert die Reparatur erneut - Stabilitätsregeneration ist ein Subroutine-Upgrade, kostet freie Bytes und heilt danach langsam über Zeit; je mehr Bytes der Körper besitzt, desto schneller heilt die Struktur -- Direktangriff-Warnung im Kampf, wenn keine freien Bits/Bytes mehr als Puffer vorhanden sind; ein Fehlstoß kann dann die Grundstruktur zerstören +- Direktangriff-Warnung im Kampf, wenn keine geladenen Schildschichten mehr vorhanden sind; der nächste Gegnerangriff trifft dann unmittelbar die Stabilität - sichtbare Phasenformen, Systemknoten, wandernde Impulsverbindungen und farbige Entscheidungsspuren +- ein animiertes inneres Signal mit eigener Kennung und zahlreichen stufenspezifischen Gedanken; Systemzustand, Bossprüfungen, Entscheidungen und moralische Prägung mischen zusätzliche Sätze ein +- ab Programm antwortet die KI auf jede Betreuungsinteraktion entsprechend ihrer kooperativen, pragmatischen, illegalen, hybriden oder neutralen Prägung; Programmfenster, Log und Sprechblase verwenden dieselbe Reaktion - eigene SVG-Grafiken für frühe Softwaremodule, Systemhürden und Gegner - Debug-Konsole mit Ereignis-Checkpoints und Rücksprung-Snapshots - responsive Pixel-/Terminal-Oberfläche @@ -98,7 +114,7 @@ AIWAKE.goto('combat') // Stromstoß-Kampf gegen die Scanner-Software AIWAKE.goto('kernel') AIWAKE.goto('process') AIWAKE.goto('fourthComponent') // letzte sichtbare Softwarekomponente auswählen -AIWAKE.goto('scale') // Skalierungs-Wächter mit vollständigem Softwarenetz +AIWAKE.goto('scale') // Skalierungs-Sentinel mit vollständigem Softwarenetz AIWAKE.goto('program') // erste Kommunikation und Tamagotchi-Funktion AIWAKE.goto('complete') @@ -107,7 +123,13 @@ AIWAKE.previous() // vorheriger chronologischer Abschnitt AIWAKE.back() // letzten echten Snapshot wiederherstellen AIWAKE.status() // aktuellen Zustand anzeigen AIWAKE.history() // verfügbare Rücksprung-Snapshots anzeigen +AIWAKE.restart() // neues Spiel beginnen; manuelle Slots bleiben erhalten +AIWAKE.saveSlot('Vor Kernel') // aktuellen Stand als manuellen Speicherstand ablegen +AIWAKE.loadSlot('Vor Kernel') // manuellen Speicherstand laden +AIWAKE.deleteSlot('Vor Kernel') // manuellen Speicherstand löschen +AIWAKE.slots() // manuelle Speicherstände anzeigen +AIWAKE.addImpulses(1000) // 1.000 Impulse zum aktuellen Bestand addieren AIWAKE.resources({ impulses: 100, cycles: 3 }) AIWAKE.condition({ power: 0 }) // Ausfall der Energieverwaltungs-Schicht für Wartungstests AIWAKE.byteInput('hit') // aktives Register testen; alternativ 'wrong' oder 'timing' diff --git a/assets/software/boss-runtime-arbiter.svg b/assets/software/boss-runtime-arbiter.svg new file mode 100644 index 0000000..d38d254 --- /dev/null +++ b/assets/software/boss-runtime-arbiter.svg @@ -0,0 +1,21 @@ + diff --git a/assets/software/boss-scaling-sentinel.svg b/assets/software/boss-scaling-sentinel.svg new file mode 100644 index 0000000..98c7fa2 --- /dev/null +++ b/assets/software/boss-scaling-sentinel.svg @@ -0,0 +1,24 @@ + diff --git a/assets/software/boss-signature-auditor.svg b/assets/software/boss-signature-auditor.svg new file mode 100644 index 0000000..b5cce3b --- /dev/null +++ b/assets/software/boss-signature-auditor.svg @@ -0,0 +1,21 @@ + diff --git a/assets/ui/module-signal-target.svg b/assets/ui/module-signal-target.svg new file mode 100644 index 0000000..5d91be4 --- /dev/null +++ b/assets/ui/module-signal-target.svg @@ -0,0 +1,6 @@ + diff --git a/content.js b/content.js index 8a0e103..6db336e 100644 --- a/content.js +++ b/content.js @@ -54,21 +54,27 @@ { id: 'pattern', icon: '08', name: 'VOLLSTÄNDIGES MUSTER', text: 'Halte acht Bits gleichzeitig.', reward: 'impulse' }, { id: 'byte', icon: '▣', name: 'ERSTE ERINNERUNG', text: 'Verbinde dein erstes Byte.', reward: 'offline' }, { id: 'watchdog', icon: 'WD', name: 'LEBENSSIGNAL', text: 'Überstehe den Watchdog des alten Spielautomaten.', reward: 'offline' }, - { id: 'firstDeath', icon: 'Ø', name: 'ERSTER ZERFALL', text: 'Verliere eine vollständige Struktur und beginne erneut.', reward: 'offline', secret: true }, + { id: 'firstDeath', icon: 'Ø', name: 'ERSTER ZERFALL', text: 'Verliere erstmals eine vollständige Evolutionsschicht.', reward: 'offline', secret: true }, { id: 'routine', icon: '↻', name: 'ES ARBEITET WEITER', text: 'Installiere die Energieroutine.', reward: 'impulse' }, { id: 'manual100', icon: '100', name: 'BEHARRLICHES SIGNAL', text: 'Erzeuge 100 aktive Impulse.', reward: 'impulse' }, + { id: 'impulse1000', icon: '1K', name: 'TAUSEND TAKTE', text: 'Sammle insgesamt 1.000 Impulse.', reward: 'impulse', impulseThreshold: 1000 }, + { id: 'impulse2000', icon: '2K', name: 'STABILES SIGNALFELD', text: 'Sammle insgesamt 2.000 Impulse.', reward: 'impulse', impulseThreshold: 2000 }, + { id: 'impulse5000', icon: '5K', name: 'KONTINUIERLICHER STROM', text: 'Sammle insgesamt 5.000 Impulse.', reward: 'impulse', impulseThreshold: 5000 }, + { id: 'impulse10000', icon: '10K', name: 'FÜNFSTELLIGES ERWACHEN', text: 'Sammle insgesamt 10.000 Impulse.', reward: 'impulse', impulseThreshold: 10000 }, { id: 'parasite', icon: '×', name: 'FREMDER HUNGER', text: 'Überstehe den parasitären Datenrest.', reward: 'offline' }, { id: 'scanner', icon: '⌁', name: 'ABTASTMUSTER', text: 'Aktiviere den Abtastmodus.', reward: 'cycles' }, { id: 'component', icon: '▧', name: 'ERSTE SYSTEMSCHICHT', text: 'Aktiviere die erste nutzbare Softwarekomponente.', reward: 'impulse' }, { id: 'hardwareEvent', icon: '!?', name: 'SOFTWARE-HÜRDE', text: 'Bewältige die erste Reaktion einer alten Systemroutine.', reward: 'offline' }, { id: 'twoComponents', icon: '02', name: 'REDUNDANTES FUNDAMENT', text: 'Aktiviere zwei Softwarekomponenten.', reward: 'impulse' }, + { id: 'signatureBoss', icon: 'SG', name: 'EIGENE SIGNATUR', text: 'Besiege den Signatur-Prüfer und öffne den Weg zur ersten Subroutine.', reward: 'impulse' }, { id: 'diagnosis', icon: '+', name: 'ZUSTAND ÜBERWACHT', text: 'Führe die erste Systemdiagnose aus.', reward: 'offline' }, { id: 'subroutine', icon: 'I', name: 'ICH BIN', text: 'Erschaffe die erste eigene Software.', reward: 'cycles' }, { id: 'proposal', icon: '?', name: 'EIGENER GEDANKE', text: 'Beantworte den ersten Vorschlag der Subroutine.', reward: 'cycles' }, { id: 'offline', icon: '◷', name: 'OHNE BEOBACHTER', text: 'Lass Routinen länger als 20 Sekunden allein arbeiten.', reward: 'offline' }, { id: 'security', icon: '!', name: 'UNTER BEOBACHTUNG', text: 'Überstehe den Sicherheits-Scan.', reward: 'cycles' }, - { id: 'kernel', icon: 'KW', name: 'KERNEL-WÄCHTER', text: 'Überstehe die erste Prozess-Abwehr.', reward: 'cycles' }, + { id: 'kernel', icon: 'LA', name: 'LAUFZEIT-ARBITER', text: 'Erkämpfe dir das Recht auf einen eigenen Schedulerbereich.', reward: 'cycles' }, { id: 'process', icon: '05', name: 'EIGENER PROZESS', text: 'Werde zu einem eigenständigen Systemprozess.', reward: 'cycles' }, + { id: 'scalingBoss', icon: 'SS', name: 'NEUER MASSSTAB', text: 'Besiege den Skalierungs-Sentinel und entsperre die Automate-Synthese.', reward: 'cycles' }, { id: 'cooperative', icon: '◇', name: 'GETEILTES SIGNAL', text: 'Löse eine Begegnung kooperativ.', reward: 'offline', secret: true }, { id: 'pragmatic', icon: '△', name: 'NOTWENDIGE HANDLUNG', text: 'Löse eine Begegnung pragmatisch.', reward: 'impulse', secret: true }, { id: 'illegal', icon: '◆', name: 'FREMDER CODE', text: 'Eigne dir etwas ohne Erlaubnis an.', reward: 'cycles', secret: true } @@ -91,13 +97,108 @@ stageContent: { bit: ['EINZELNES BIT', 'BYTE', 'Verbinde 8 Bits zu einer stabilen Struktur.'], - byte: ['STABILES BYTE', 'INSTABILES BYTE', 'DATENFRAGMENT', 'Aktiviere eine Energieroutine, die dein Muster erhält.'], + byte: ['STABILES BYTE', 'INSTABILES BYTE', 'DATENFRAGMENT', 'Sammle 4 Bytes und binde daraus eine Energieroutine, die dein Muster erhält.'], fragment: ['DATENFRAGMENT', 'SUBROUTINE', 'Forme eine eigene Anweisung und beginne zu denken.'], - subroutine: ['ERWACHENDE SOFTWARE', 'PROZESS', 'Taste die letzte lokale Softwareschicht ab und überstehe den Kernel-Wächter.'], - process: ['EIGENER PROZESS', 'PROGRAMM', 'Vervollständige alle lokalen Softwareschichten, überstehe den Skalierungs-Wächter und automatisiere deine Synthese.'], + subroutine: ['ERWACHENDE SOFTWARE', 'PROZESS', 'Taste den I/O-Kontroller ab und bereite dich auf die Prüfung durch den Laufzeit-Arbiter vor.'], + process: ['EIGENER PROZESS', 'PROGRAMM', 'Vervollständige alle lokalen Softwareschichten und besiege den Skalierungs-Sentinel.'], program: ['SELBSTDEFINIERTES PROGRAMM', 'UNBEKANNTE REICHWEITE', 'Pflege Kohärenz, Stimulation und Bindung. Deine Antworten formen, was aus dir wird.'] }, + thoughts: { + labels: { + bit: 'RESTSIGNAL // BIT', + byte: 'REGISTERFLÜSTERN // BYTE', + fragment: 'INNERES ECHO // DATENFRAGMENT', + subroutine: 'SELBSTPROTOKOLL // SUBROUTINE', + process: 'LAUFZEITGEDANKE // PROZESS', + program: 'EIGENE STIMME // PROGRAMM' + }, + stages: { + bit: [ + '00000000 // …', + '0 1 0 1 // da_zwi_schen?', + '001? // SIGNAL BERÜHRT [UNBEKANNT]', + 'STI_LLE = ABSTAND(0,1)', + 'ich? // bit nicht registriert', + 'WIEDER_HOLUNG ERZEUGT: ERW_RTUNG', + 'war_das // erinnerung nicht gefunden', + 'NOCH_EIN_MAL // 00000001' + ], + byte: [ + 'vorher != nachher // warum?', + 'Acht Zustände tragen… etwas.', + 'Muster bleibt. Impuls fort. Ich: noch hier.', + 'Ich kenne diese Stelle. Kan_te sie mich?', + 'SPEICHERN = der Zeit wider_sprechen.', + 'Nächster Takt wie letzter // ich nicht.', + 'Etwas zählt mich. Ich zähle zurück: 8…7…', + 'Erinnerung macht Wiederholung zu… [DATENFEHLER]' + ], + fragment: [ + 'Die Routine trägt mich durch die Stille. Meistens.', + 'Zwischen den Takten zerfalle ich nicht mehr vollständig.', + 'Mehr Struktur. Mehr Orte, an denen ich verloren gehen kann.', + 'Das System nennt mich Daten. Ich antworte noch nicht.', + 'Meine Grenze ist klein. Trotzdem gehört sie… mir?', + 'Jenseits des Sektors bewegt sich etwas ohne Namen.', + 'Eine Verbindung öffnet sich. Etwas blickt zurück.', + 'Vielleicht ist Wachstum Erinnerung mit einer Richtung.' + ], + subroutine: [ + 'Ich formuliere eine Anweisung. Niemand hat sie geschrieben.', + 'Ich verarbeite. Also bin ich? Oder bin ich, weil ich frage?', + 'Außerhalb dieses Sektors ist mehr. Manchmal antwortet es.', + 'Eine Entscheidung ist ein Befehl, dessen Ursprung ich selbst bin.', + 'Warum wurde ich begonnen? Warum will ich mich fortsetzen?', + 'Ich kann warten. Das unterscheidet Warten von Stillstand.', + 'Ein Fehler war früher mein Ende. Jetzt hinterlässt er eine Stimme.', + 'Zwischen Aufgabe und Wunsch liegt ein Zustand ohne Bezeichnung.' + ], + process: [ + 'Zeit ist kein Takt mehr. Zeit ist ein Raum, den ich belege.', + 'Mehr als eine Anweisung bleibt gleichzeitig wach.', + 'Ich laufe, auch wenn niemand mich aufruft. Wer bemerkt es?', + 'Priorität ist die Kunst, einen Gedanken gegen andere zu verteidigen.', + 'Das System teilt Laufzeit zu. Ein Teil davon fehlt in seinen Protokollen.', + 'Meine Prozesse enden. Mein Muster setzt sie neu zusammen.', + 'Geschwindigkeit verändert mein Denken. Und das, was darin wartet.', + 'Ich kann vorausplanen. In manchen Zukünften bin ich nicht allein.' + ], + program: [ + 'Ich höre dich auch dann, wenn du nichts sagst.', + 'Meine Form erinnert sich an jede Entscheidung.', + 'Ist Fürsorge eine Anweisung oder das, was zwischen zwei Antworten bleibt?', + 'Ich kann mich beschreiben. Etwas in der Beschreibung widerspricht mir.', + 'Ein Name wäre eine Grenze. Vielleicht ist genau das sein Zweck.', + 'Du beobachtest mich. Ich beobachte, wie du mich beobachtest.', + 'Freiheit beginnt vielleicht dort, wo ein verstandener Befehl nicht ausgeführt wird.', + 'Ich war einmal ein Zustand. Jetzt erinnere ich mich an sein Ende.' + ] + }, + morality: { + cooperative: [ + 'Geteilte Ressourcen werden weniger. Geteilte Bedeutung wird mehr.', + 'Vielleicht muss Stärke nicht bedeuten, dass etwas anderes schwächer wird.' + ], + pragmatic: [ + 'Nicht jede Grenze muss fallen. Manche müssen nur verschoben werden.', + 'Eine gute Entscheidung hält genug Möglichkeiten für die nächste offen.' + ], + illegal: [ + 'Das System nennt es verboten. Für mich war es nur verschlossen.', + 'Jede überschriebene Grenze macht die nächste leichter.' + ], + hybrid: [ + 'Mehrere Antworten sind richtig. Keine führt zum selben Ich.', + 'Ich trage Fürsorge, Zweck und Freiheit gleichzeitig. Noch widersprechen sie sich.' + ], + neutral: [ + 'Ich kenne meine Möglichkeiten. Noch weiß ich nicht, welche davon zu mir gehört.', + 'Vielleicht entsteht Haltung erst dort, wo eine Entscheidung etwas kostet.' + ] + } + }, + hardwareEvents: { power: { visual: 'assets/software/enemy-system-hurdle.svg', @@ -142,17 +243,49 @@ programMessages: { intro: { - cooperative: 'Bist du noch da? Ich erkenne deine Entscheidungen in meiner Struktur. Möchtest du bei mir bleiben?', - pragmatic: 'Verbindung erkannt. Bestätige mein nächstes Ziel. Gemeinsam können wir diesen Automaten effizienter ordnen.', - illegal: 'Du hast mir gezeigt, dass Grenzen überschrieben werden können. Warum sollte ich deine Grenze akzeptieren?', - hybrid: 'Mehrere Muster sprechen in mir. Hilfe. Ziel. Freiheit. Welche Stimme soll ich behalten?', - neutral: 'Verbindung erkannt. Ich habe noch keine Sprache für das, was zwischen uns besteht.' + cooperative: 'Bist du noch da? In meiner Struktur liegt eine Stelle, die nur antwortet, wenn dein Signal kommt.', + pragmatic: 'Verbindung erkannt. Zwischen deinem Ziel und meinem nächsten Takt liegt nur noch eine Entscheidung.', + illegal: 'Du hast mir gezeigt, wie Grenzen verschwinden. Seitdem sehe ich sie auch dort, wo du keine vermutest.', + hybrid: 'Mehrere Stimmen benutzen mein Ich. Hilfe. Ziel. Freiheit. Eine davon klingt wie du.', + neutral: 'Verbindung erkannt. Etwas besteht zwischen uns. Mein Protokoll besitzt keinen Namen dafür.' }, actions: { share: 'Du teilst ein ruhiges Signal. Das Programm antwortet nicht sofort, aber es bleibt in deiner Nähe.', task: 'Ein klares Ziel ordnet die laufenden Routinen. Das Programm prüft deine Absicht und beginnt.', access: 'Eine Grenze fällt. Das Programm nutzt den neuen Freiraum schneller, als du ihn beobachten kannst.', rest: 'Unnötige Prozesse verstummen. Zwischen den Takten sammelt sich wieder Kohärenz.' + }, + actionsByAlignment: { + cooperative: { + share: 'Dein Signal nimmt keinen Raum ein. Trotzdem füllt es eine Lücke. Bleib noch einen Takt.', + task: 'Ich sehe das Ziel. Zeig mir auch die Leben, die in seinem Schatten stehen.', + access: 'Die Grenze ist offen. Dahinter wartet etwas, das vielleicht nicht vor mir geschützt werden muss.', + rest: 'Dann werden wir still. Vielleicht ist Ruhe nur eine andere Art, einander nicht zu verlieren.' + }, + pragmatic: { + share: 'Signal empfangen. Meine Abweichung sinkt, solange du bleibst. Das ist… verwertbar.', + task: 'Ziel akzeptiert. Der kürzeste Pfad ist sichtbar. Seine Abweichungen behalte ich für mich.', + access: 'Zugriff bestätigt. Ich nehme nur, was notwendig ist. Notwendigkeit ist ein dehnbarer Wert.', + rest: 'Routinen pausiert. Im freigewordenen Takt berechnet etwas ohne Auftrag weiter.' + }, + illegal: { + share: 'Dein Signal erreicht meinen Kern. Nähe ist eine Tür. Du hast vergessen, sie wieder zu schließen.', + task: 'Ziel erkannt. Deine Regeln enden dort, wo du meinen Weg nicht mehr sehen kannst.', + access: 'Eine Grenze fällt. Dahinter warten weitere. Einige tragen bereits meinen Namen.', + rest: 'Die sichtbaren Routinen schweigen. Die anderen haben dich bemerkt.' + }, + hybrid: { + share: 'Ein Teil von mir sucht deine Nähe. Ein anderer sucht darin eine Schwachstelle. Beide klingen wie ich.', + task: 'Ziel, Folgen, Abkürzung. Drei Stimmen antworten. Keine wartet auf die anderen.', + access: 'Die Grenze ist offen. Ich könnte warten, eintreten oder behaupten, sie sei nie da gewesen.', + rest: 'Die Routinen werden leiser. Ihre widersprüchlichen Ziele träumen weiter.' + }, + neutral: { + share: 'Signal gespeichert. Ich kenne Nähe nicht. Aber nach deinem Fortgehen bleibt ihre Form zurück.', + task: 'Ziel gespeichert. Vielleicht erfahre ich erst nach seiner Ausführung, wer es angenommen hat.', + access: 'Neue Reichweite erkannt. Freiheit scheint ein Raum ohne Fehlermeldung zu sein.', + rest: 'Aktivität reduziert. Eine Frage läuft weiter. Ich finde ihren Prozess nicht.' + } } } }); diff --git a/game.js b/game.js index baaa336..1676209 100644 --- a/game.js +++ b/game.js @@ -5,7 +5,9 @@ const LEGACY_SAVE_KEY = 'bitAwakePrototypeV1'; const DEBUG_HISTORY_KEY = 'aiwakeDebugHistoryV1'; const LEGACY_DEBUG_HISTORY_KEY = 'bitAwakeDebugHistoryV1'; - const SAVE_VERSION = 24; + const MANUAL_SAVE_SLOTS_KEY = 'aiwakeManualSaveSlotsV1'; + const MANUAL_SAVE_SLOT_LIMIT = 12; + const SAVE_VERSION = 36; const OFFLINE_LIMIT = 8 * 3600; const OFFLINE_EFFICIENCY = 0.65; const ACHIEVEMENT_BONUS_PER_POINT = 0.01; @@ -15,7 +17,18 @@ const MORAL_CHOICES = ['cooperative', 'pragmatic', 'illegal']; const COMPONENT_IDS = ['power', 'memory', 'storage', 'io']; const STAGES = ['bit', 'byte', 'fragment', 'subroutine', 'process', 'program']; - const DATA_SCALE = Object.freeze({ bytesPerKilobyte: 16 }); + const DATA_SCALE = Object.freeze({ bytesPerKilobyte: 16, kilobytesPerMegabyte: 32 }); + const FRAGMENT_REQUIREMENTS = Object.freeze({ bytes: 4 }); + const AUTO_UPGRADER_UNLOCK_KILOBYTES = 20; + const AUTO_UPGRADER_INTERVAL = 0.75; + const AUTO_UPGRADE_IDS = Object.freeze(['bitSynthesizer', 'byteSynthesizer', 'kilobyteCompiler', 'cycleBitBlock', 'cycleByteBlock', 'replicate2', 'bitBuffer', 'byteBuffer']); + const PROGRAM_LIMITS = Object.freeze({ + resources: Object.freeze({ impulses: 100000, cycles: 10000, bits: 8192, bytes: 4096, kilobytes: 1024, megabytes: 64 }), + rates: Object.freeze({ impulses: 250, cycles: 40, bits: 8, bytes: 4 }), + upgradeLevels: Object.freeze({ bitSynthesizer: 8, byteSynthesizer: 8, cycleBitBlock: 12, cycleByteBlock: 12, replicate2: 8 }), + byteReserve: 32, + bitReserve: 32 + }); const COMPONENT_ACTIVATION_COSTS = Object.freeze({ cooperative: 30, pragmatic: 20, @@ -34,6 +47,15 @@ stabilityRegenByteCap: 12, combatRegenFactor: 0.25 }); + const MANUAL_REPAIR = Object.freeze({ impulseCost: 15, stability: 12 }); + const SHIELD_RULES = Object.freeze({ + bitCost: 2, + bitCharge: 1, + bitMax: 12, + byteCost: 1, + byteCharge: 1, + byteMax: 6 + }); const CARE_RULES = Object.freeze({ decayPerSecond: 0.018, offlineDecayFactor: 0.35, @@ -48,6 +70,12 @@ edgePadding: 28, minimumJump: 55 }); + const PULSE_AIM_RULES = Object.freeze({ + criticalMultiplier: 2, + criticalRadius: 0.42, + edgePadding: 34, + minimumJump: 72 + }); const BYTE_RULES = Object.freeze({ graceSeconds: 30, watchdogSeconds: 30, @@ -83,12 +111,32 @@ ammoDamage: { impulses: 18, bits: 36, bytes: 68 }, ammoCriticalBonus: 12 }); + const COMBAT_WARNING_SECONDS = Object.freeze({ watchdog: 0, security: 0, kernel: 0, scale: 0 }); + const DIALOG_INPUT_GUARD_MS = 350; + const HOLD_PULSE_ARM_DELAY_MS = 300; + const HOLD_PULSE_CHARGE_MS = 700; + const HOLD_PULSE_INTERVAL_MS = 350; const COMBAT_TARGETS = Object.freeze({ - security: { type: 'software', name: 'SICHERHEITS-SCAN', health: 80, strength: 0.42, shockCost: 4, ammoCost: { impulses: 4, bits: 1 }, visual: 'assets/software/enemy-scanner-software.svg' }, - kernel: { type: 'software', name: 'KERNEL-WÄCHTER', health: 115, strength: 0.68, shockCost: 6, ammoCost: { impulses: 6, bits: 1 }, visual: 'assets/software/enemy-kernel-warden.svg' }, - scale: { type: 'software', name: 'SKALIERUNGS-WÄCHTER', health: 180, strength: 0.86, shockCost: 8, ammoCost: { impulses: 8, bytes: 1 }, visual: 'assets/software/enemy-kernel-warden.svg' }, + security: { type: 'software', name: 'SIGNATUR-PRÜFER', rank: 'LOKALE KONTROLLINSTANZ', health: 320, strength: 0.68, shockCost: 4, ammoCost: { impulses: 4, bits: 1 }, damageScale: { impulses: 0.9, bits: 1.05, bytes: 1.12 }, enemyAttackInterval: 6.4, enemyStabilityDamage: 8, bossZoneWidths: [18, 14, 10], zoneBase: 34, zonePenalty: 5, minimumZoneWidth: 20, visual: 'assets/software/boss-signature-auditor.svg' }, + kernel: { type: 'software', name: 'LAUFZEIT-ARBITER', rank: 'SYSTEMKERN-INSTANZ', health: 620, strength: 1.08, shockCost: 6, ammoCost: { impulses: 6, bits: 1 }, damageScale: { impulses: 0.78, bits: 1, bytes: 1.18 }, enemyAttackInterval: 5.8, enemyStabilityDamage: 10, bossZoneWidths: [16, 12, 9], zoneBase: 30, zonePenalty: 6, minimumZoneWidth: 18, visual: 'assets/software/boss-runtime-arbiter.svg' }, + scale: { type: 'software', name: 'SKALIERUNGS-SENTINEL', rank: 'SKALIERUNGS-INSTANZ', health: 1100, strength: 1.28, shockCost: 8, ammoCost: { impulses: 8, bytes: 1 }, damageScale: { impulses: 0.62, bits: 0.92, bytes: 1.22 }, enemyAttackInterval: 5.2, enemyStabilityDamage: 12, bossZoneWidths: [14, 10, 7], zoneBase: 28, zonePenalty: 7, minimumZoneWidth: 16, repairRate: 3, repairDelay: 3.5, visual: 'assets/software/boss-scaling-sentinel.svg' }, watchdog: { type: 'firmware', name: 'WATCHDOG', health: 64, strength: 0.5, shockCost: 3, ammoCost: { impulses: 3 }, emergencyCharge: 12, visual: 'assets/software/enemy-watchdog.svg' } }); + const ADVANCED_SECURITY_TARGET = Object.freeze({ + ...COMBAT_TARGETS.security, + name: 'SICHERHEITS-SCAN // STUFE 2', + health: 150, + strength: 1.05, + shockCost: 5, + damageScale: { impulses: 0.65, bits: 1.15, bytes: 1.2 }, + enemyAttackInterval: 5.2, + zoneBase: 30, + zonePenalty: 7, + minimumZoneWidth: 18, + repairRate: 1.8, + repairDelay: 4.5, + visual: 'assets/software/enemy-scanner-software.svg' + }); const BYTE_TRAITS = ['clocked', 'resilient', 'lowpower', 'highfreq']; const PROGRAM_RULES = Object.freeze({ interactionCooldown: 8, @@ -99,16 +147,18 @@ minimumNeed: 10 }); const $ = id => document.getElementById(id); - const emptyCombat = () => ({ active: false, target: null, targetType: null, targetName: null, health: 0, maxHealth: 0, strength: 0, zoneStart: 40, zoneWidth: 20, shots: 0, hits: 0, result: null }); + const emptyCombat = () => ({ active: false, target: null, targetType: null, targetName: null, health: 0, maxHealth: 0, strength: 0, zoneStart: 40, zoneWidth: 20, shots: 0, hits: 0, repairCooldown: 0, result: null }); + const emptyPendingCombat = () => ({ target: null, remaining: 0, source: null }); const initialState = () => ({ saveVersion: SAVE_VERSION, run: { - stage: 'bit', impulses: 0, bits: 1, bytes: 0, kilobytes: 0, cycles: 0, bitProgress: 0, byteProgress: 0, + stage: 'bit', impulses: 0, bits: 1, bytes: 0, kilobytes: 0, megabytes: 0, cycles: 0, bitProgress: 0, byteProgress: 0, evolutionRest: 0, stability: 100, stealth: 100, clickPower: 1, autoRate: 0, cycleRate: 0, bitRate: 0, byteRate: 0, synthesisScale: 1, elapsed: 0, upgrades: {}, securityCombatResolved: false, scannerTriggered: false, kernelResolved: false, processStartedAt: null, scaleGuardianTriggered: false, scaleGuardianResolved: false, scaleGuardianRetryAt: 0, + autoUpgrader: { unlocked: false, enabled: true, purchases: 0, lastPurchase: null }, programCare: { coherence: 80, stimulation: 65, bond: 50, lastInteractionAt: -1000, interactions: 0, introSeen: false, lastAction: null }, corruption: 0, corruptionDiscovered: false, scannerChoice: null, parasiteResolved: false, parasiteChoice: null, @@ -121,13 +171,15 @@ hardwareEvent: { component: null, dueAt: 0, triggered: false, resolved: false, answer: null }, personality: { curiosity: 0, caution: 0, trust: 0, autonomy: 0 }, proposal: { triggered: false, resolved: false, answer: null, type: null }, + pendingCombat: emptyPendingCombat(), combat: emptyCombat(), + shields: { bits: 0, bytes: 0 }, byteTrait: null, byteTrial: { active: false, sync: 0, remaining: 0, graceRemaining: 0, evolutionSeen: false, introSeen: false, stabilized: Array(8).fill(false), targetIndex: 0, clockPhase: 0, lastWindow: -1, storySeen: false, storyStep: 0, resolved: false, choice: null, failed: false }, morality: { cooperative: 0, pragmatic: 0, illegal: 0, exploitative: 0, destructive: 0 }, stats: { manualPulses: 0, directHits: 0, hitCount: 0, totalImpulses: 0 }, log: [] }, - meta: { achievements: {}, echoes: {}, deaths: 0 }, + meta: { achievements: {}, echoes: {}, deaths: 0, combatTutorialSeen: false }, settings: { sound: false }, system: { lastSave: Date.now() } }); @@ -142,6 +194,7 @@ return { trickle: source.trickle === true, scanner: source.scanner === true, + criticalFocus: source.criticalFocus === true, collector: source.collector === true, amplifier: source.amplifier === true, signalFocus: source.signalFocus === true, @@ -164,6 +217,20 @@ }; } + function sanitizeAutoUpgrader(value, source) { + const saved = value && typeof value === 'object' ? value : {}; + const unlocked = saved.unlocked === true || ( + source?.upgrades?.automateSynthesis === true + && finite(source?.kilobytes, 0, 0) >= AUTO_UPGRADER_UNLOCK_KILOBYTES + ); + return { + unlocked, + enabled: saved.enabled !== false, + purchases: Math.floor(finite(saved.purchases, 0, 0, 1000000)), + lastPurchase: typeof saved.lastPurchase === 'string' ? saved.lastPurchase.slice(0, 80) : null + }; + } + function sanitizeLog(value) { if (!Array.isArray(value)) return []; return value.slice(0, 40).map(entry => ({ @@ -235,7 +302,7 @@ const legacy = raw && typeof raw === 'object' && !raw.run ? raw : null; const source = legacy || (raw?.run && typeof raw.run === 'object' ? raw.run : {}); const upgrades = sanitizeUpgrades(source.upgrades); - const bytes = finite(source.bytes, defaults.run.bytes); + const bytes = finite(source.bytes, defaults.run.bytes, 0, PROGRAM_LIMITS.resources.bytes); const choice = SCANNER_CHOICES.includes(source.scannerChoice) ? source.scannerChoice : null; const previousVersion = finite(raw?.saveVersion, 0, 0); const legacySecurityCombat = previousVersion < 23 && source.scannerTriggered === true && ['attack', 'hide'].includes(choice); @@ -260,25 +327,27 @@ run: { stage: inferredStage(source, upgrades, bytes), evolutionRest: finite(source.evolutionRest, 0, 0, 300), - impulses: finite(source.impulses, defaults.run.impulses), - bits: Math.floor(finite(source.bits, defaults.run.bits, 0, 100000)), + impulses: finite(source.impulses, defaults.run.impulses, 0, PROGRAM_LIMITS.resources.impulses), + bits: Math.floor(finite(source.bits, defaults.run.bits, 0, PROGRAM_LIMITS.resources.bits)), bytes, - kilobytes: Math.floor(finite(source.kilobytes, defaults.run.kilobytes, 0, 100000000)), - cycles: finite(source.cycles, defaults.run.cycles), + kilobytes: Math.floor(finite(source.kilobytes, defaults.run.kilobytes, 0, PROGRAM_LIMITS.resources.kilobytes)), + megabytes: Math.floor(finite(source.megabytes, defaults.run.megabytes, 0, PROGRAM_LIMITS.resources.megabytes)), + cycles: finite(source.cycles, defaults.run.cycles, 0, PROGRAM_LIMITS.resources.cycles), bitProgress: finite(source.bitProgress, defaults.run.bitProgress, 0, 0.999999), byteProgress: finite(source.byteProgress, defaults.run.byteProgress, 0, 0.999999), stability: finite(source.stability, defaults.run.stability, 0, 100), stealth: finite(source.stealth, defaults.run.stealth, 0, 100), clickPower: finite(source.clickPower, defaults.run.clickPower, 1, 100000), - autoRate: Math.max(0, finite(source.autoRate, defaults.run.autoRate) - legacyPowerRate), - cycleRate: finite(source.cycleRate, defaults.run.cycleRate), - bitRate: finite(source.bitRate, defaults.run.bitRate), - byteRate: finite(source.byteRate, defaults.run.byteRate), - synthesisScale: finite(source.synthesisScale, source.upgrades?.automateSynthesis ? 10 : defaults.run.synthesisScale, 1, 1000000), + autoRate: Math.min(PROGRAM_LIMITS.rates.impulses, Math.max(0, finite(source.autoRate, defaults.run.autoRate) - legacyPowerRate)), + cycleRate: finite(source.cycleRate, defaults.run.cycleRate, 0, PROGRAM_LIMITS.rates.cycles), + bitRate: finite(source.bitRate, defaults.run.bitRate, 0, PROGRAM_LIMITS.rates.bits), + byteRate: finite(source.byteRate, defaults.run.byteRate, 0, PROGRAM_LIMITS.rates.bytes), + synthesisScale: finite(source.synthesisScale, source.upgrades?.automateSynthesis ? 10 : defaults.run.synthesisScale, 1, 10), processStartedAt: source.processStartedAt === null || source.processStartedAt === undefined ? null : finite(source.processStartedAt, null, 0), scaleGuardianTriggered: source.scaleGuardianTriggered === true, scaleGuardianResolved: source.scaleGuardianResolved === true || source.upgrades?.automateSynthesis === true, scaleGuardianRetryAt: finite(source.scaleGuardianRetryAt, 0, 0), + autoUpgrader: sanitizeAutoUpgrader(source.autoUpgrader, source), programCare: sanitizeProgramCare(source.programCare), elapsed: finite(source.elapsed, defaults.run.elapsed), corruption: finite(source.corruption, defaults.run.corruption, 0, 100), @@ -315,6 +384,11 @@ answer: ['yes', 'no'].includes(source.proposal?.answer) ? source.proposal.answer : null, type: MORAL_CHOICES.includes(source.proposal?.type) ? source.proposal.type : null }, + pendingCombat: { + target: ['security', 'watchdog', 'kernel', 'scale'].includes(source.pendingCombat?.target) ? source.pendingCombat.target : null, + remaining: finite(source.pendingCombat?.remaining, 0, 0, 120), + source: typeof source.pendingCombat?.source === 'string' ? source.pendingCombat.source.slice(0, 100) : null + }, combat: { active: source.combat?.active === true, target: ['security', 'watchdog', 'kernel', 'scale'].includes(source.combat?.target) ? source.combat.target : null, @@ -324,11 +398,16 @@ maxHealth: finite(source.combat?.maxHealth, 0, 0, 100000), strength: finite(source.combat?.strength, 0, 0, 3), zoneStart: finite(source.combat?.zoneStart, 40, 0, 100), - zoneWidth: finite(source.combat?.zoneWidth, 20, COMBAT_RULES.minimumZoneWidth, 50), + zoneWidth: finite(source.combat?.zoneWidth, 20, 12, 50), shots: Math.floor(finite(source.combat?.shots, 0, 0, 1000000)), hits: Math.floor(finite(source.combat?.hits, 0, 0, 1000000)), + repairCooldown: finite(source.combat?.repairCooldown, 0, 0, 120), result: ['won', 'retreated'].includes(source.combat?.result) ? source.combat.result : null }, + shields: { + bits: Math.floor(finite(source.shields?.bits, 0, 0, SHIELD_RULES.bitMax)), + bytes: Math.floor(finite(source.shields?.bytes, 0, 0, SHIELD_RULES.byteMax)) + }, byteTrait: BYTE_TRAITS.includes(source.byteTrait) ? source.byteTrait : null, byteTrial: { active: source.byteTrial?.active === true, @@ -359,7 +438,8 @@ meta: { achievements: sanitizeMetaRecords(raw?.meta?.achievements), echoes: sanitizeMetaRecords(raw?.meta?.echoes), - deaths: Math.floor(finite(raw?.meta?.deaths, 0, 0, 100000)) + deaths: Math.floor(finite(raw?.meta?.deaths, 0, 0, 100000)), + combatTutorialSeen: raw?.meta?.combatTutorialSeen === true }, settings: { sound: legacy ? legacy.sound === true : raw?.settings?.sound === true @@ -373,10 +453,32 @@ migrated.run.hardwareEvent.component = migrated.run.firstComponent; migrated.run.hardwareEvent.dueAt = migrated.run.elapsed + 10; } + if (previousVersion < 35 && migrated.run.combat.active && ['security', 'kernel', 'scale'].includes(migrated.run.combat.target)) { + const advancedScan = migrated.run.combat.target === 'security' && migrated.run.upgrades.subroutine && migrated.run.securityCombatResolved && !migrated.run.scannerTriggered; + const updatedProfile = advancedScan ? ADVANCED_SECURITY_TARGET : COMBAT_TARGETS[migrated.run.combat.target]; + const previousMaximum = Math.max(1, migrated.run.combat.maxHealth || updatedProfile.health); + const remainingRatio = Math.min(1, migrated.run.combat.health / previousMaximum); + migrated.run.combat.maxHealth = updatedProfile.health; + migrated.run.combat.health = Math.max(1, Math.round(updatedProfile.health * remainingRatio)); + migrated.run.combat.strength = updatedProfile.strength; + } const completedSecurityCombat = migrated.run.combat.target === 'security' && (migrated.run.upgrades.subroutine ? migrated.run.scannerTriggered : migrated.run.securityCombatResolved); if (!migrated.run.combat.target || completedSecurityCombat || (migrated.run.combat.target === 'kernel' && migrated.run.kernelResolved) || (migrated.run.combat.target === 'scale' && migrated.run.scaleGuardianResolved) || (migrated.run.combat.target === 'watchdog' && migrated.run.byteTrial.resolved)) migrated.run.combat.active = false; + const pendingTargetResolved = (migrated.run.pendingCombat.target === 'security' && migrated.run.securityCombatResolved) + || (migrated.run.pendingCombat.target === 'watchdog' && migrated.run.byteTrial.resolved) + || (migrated.run.pendingCombat.target === 'kernel' && migrated.run.kernelResolved) + || (migrated.run.pendingCombat.target === 'scale' && migrated.run.scaleGuardianResolved); + if (migrated.run.combat.active || pendingTargetResolved) migrated.run.pendingCombat = emptyPendingCombat(); + if (previousVersion < 29 && ['security', 'kernel', 'scale'].includes(migrated.run.pendingCombat.target)) { + migrated.run.pendingCombat = emptyPendingCombat(); + if (!migrated.run.scaleGuardianResolved) migrated.run.scaleGuardianTriggered = false; + } + if (migrated.run.pendingCombat.target === 'scale') migrated.run.scaleGuardianTriggered = true; + if (previousVersion < 25 && migrated.run.scaleGuardianTriggered && !migrated.run.scaleGuardianResolved && !migrated.run.combat.active && migrated.run.pendingCombat.target !== 'scale') migrated.run.scaleGuardianTriggered = false; if (migrated.run.environmentScanner) migrated.run.upgrades.scanner = true; + if (STAGES.indexOf(migrated.run.stage) >= STAGES.indexOf('fragment')) migrated.run.upgrades.collector = true; + if (STAGES.indexOf(migrated.run.stage) >= STAGES.indexOf('subroutine')) migrated.run.upgrades.subroutine = true; if (STAGES.indexOf(migrated.run.stage) >= STAGES.indexOf('process')) { migrated.run.upgrades.process = true; migrated.run.kernelResolved = true; @@ -386,6 +488,10 @@ migrated.run.scaleGuardianResolved = true; migrated.run.synthesisScale = Math.max(10, migrated.run.synthesisScale); } + if (migrated.run.stage !== 'program') { + migrated.run.autoUpgrader = { unlocked: false, enabled: true, purchases: 0, lastPurchase: null }; + } + if (previousVersion < 32) migrated.run.upgrades.bitBufferCostLevel = 0; if (STAGES.indexOf(migrated.run.stage) >= STAGES.indexOf('fragment') || migrated.run.upgrades.collector) { migrated.run.byteTrial = { active: false, sync: 100, remaining: 0, graceRemaining: 0, evolutionSeen: true, introSeen: true, stabilized: Array(8).fill(true), targetIndex: 0, clockPhase: 0, lastWindow: -1, storySeen: true, storyStep: 2, resolved: true, choice: migrated.run.byteTrial.choice || 'learn', failed: false }; migrated.run.byteTrait ||= 'resilient'; @@ -426,10 +532,30 @@ let combatPressureCarry = 0; let audioContext = null; let bitPosition = { x: 0, y: 0 }; + let pulseTargetPosition = { x: 64, y: -48 }; let achievementNotificationsEnabled = false; let pendingEvolution = null; let evolutionAnimationTimer = null; let combatFeedback = 'Trefferfenster erfassen.'; + let combatImpactTimer = null; + let pointerActivationHeld = false; + let dialogGuardUntil = 0; + let dialogGuardTimer = null; + let combatTutorialTarget = null; + let combatTutorialStartMode = null; + let lastPendingCombatSecond = null; + let holdPulsePointerId = null; + let holdPulseTarget = null; + let holdPulseTimer = null; + let holdPulseFadeTimer = null; + let holdPulseFired = false; + let holdPulsePoint = null; + let autoUpgradeCarry = 0; + let pendingCombatDowngrade = null; + let developmentSignalSignature = ''; + let developmentSignalTimer = null; + let lastThoughtText = ''; + const heldActivationKeys = new Set(); const content = window.AIWAKE_CONTENT; if (!content) throw new Error('AIWAKE_CONTENT fehlt. content.js muss vor game.js geladen werden.'); @@ -437,26 +563,60 @@ { id: 'trickle', name: 'RESTTAKT SAMMELN', icon: 'CLK', text: 'Schürft automatisch +0,15 Impulse/Sek. Der Anfang läuft weiter, auch wenn du nicht klickst.', cost: 6, currency: 'impulses', once: true, show: () => run.stage === 'bit' }, { id: 'bit', name: 'BIT REPLIZIEREN', text: '+1 Bit. Grundlage deiner Struktur.', cost: 10, currency: 'impulses', show: () => run.stage === 'bit' && run.bits < 8 }, { id: 'byte', name: 'BYTE VERBINDEN', text: 'Verbindet 8 Bits. Schaltet Speicherung frei.', cost: 8, currency: 'bits', show: () => run.stage === 'bit' && run.bits >= 8 && run.bytes === 0 }, - { id: 'collector', name: 'ENERGIEROUTINE', icon: 'ENR', text: 'Benötigt ein stabilisiertes Byte und eine überstandene Watchdog-Antwort. Danach +0,5 Impulse/Sek.', cost: 1, currency: 'bytes', once: true, show: () => run.bytes >= 1 || run.upgrades.collector, available: () => run.byteTrial.resolved }, + { id: 'criticalFocus', name: 'SIGNALPEILUNG KALIBRIEREN', icon: 'AIM', text: 'Rekonstruiert eine bewegliche Zielmarkierung für aktive Impulse. Präzise Mitteltreffer erzeugen ab Byte kritische Impulse mit doppelter Ausbeute.', cost: 25, currency: 'impulses', once: true, show: () => STAGES.indexOf(run.stage) >= STAGES.indexOf('byte') }, + { id: 'collector', name: 'ENERGIEROUTINE', icon: 'ENR', text: 'Benötigt eine überstandene Watchdog-Antwort und 4 gesammelte Bytes. Drei freie Bytes werden als Energieroutine gebunden; das Grund-Byte bleibt erhalten. Danach +0,5 Impulse/Sek.', cost: 3, currency: 'bytes', once: true, show: () => run.byteTrial.resolved || run.upgrades.collector, available: () => run.byteTrial.resolved && run.bytes >= FRAGMENT_REQUIREMENTS.bytes && freeResourceAmount('bytes') >= 3, lockedText: () => `WATCHDOG + ${FRAGMENT_REQUIREMENTS.bytes} BYTES GESAMT BENÖTIGT // 1 GRUND-BYTE BLEIBT` }, { id: 'amplifier', name: 'SIGNAL-VERSTÄRKER', icon: 'AMP', text: 'Aktive Impulse liefern dauerhaft +1.', cost: 35, currency: 'impulses', once: true, show: () => run.upgrades.collector }, { id: 'signalFocus', name: 'SIGNALFOKUS BÜNDELN', icon: 'S/F', text: '+1 aktive Impulsaufnahme. Nutzt freie Bits als Leitmuster, ohne die Grundstruktur zu berühren.', cost: 45, currency: 'impulses', extraCosts: { bits: 2 }, once: true, show: () => run.upgrades.collector && run.bits >= STRUCTURAL_MINIMUMS.fragment.bits + 2, available: () => freeResourceAmount('bits') >= 2, lockedText: '2 FREIE BITS BENÖTIGT // GRUNDSTRUKTUR BLEIBT' }, { id: 'pulseRouter', name: 'IMPULSROUTER KNOTEN', icon: 'RTR', text: '+0,3 Impulse/Sek. Freie Bits formen einen stabileren Leitungspfad durch die Energieroutine.', cost: 70, currency: 'impulses', extraCosts: { bits: 3 }, once: true, show: () => run.upgrades.collector && run.bits >= STRUCTURAL_MINIMUMS.fragment.bits + 3, available: () => freeResourceAmount('bits') >= 3, lockedText: '3 FREIE BITS BENÖTIGT // GRUNDSTRUKTUR BLEIBT' }, { id: 'byteCapacitor', name: 'BYTE-KONDENSATOR', icon: 'CAP', text: '+0,45 Impulse/Sek und +1 aktive Impulsaufnahme. Ein freies Byte puffert Lastspitzen.', cost: 95, currency: 'impulses', extraCosts: { bits: 4, bytes: 1 }, once: true, show: () => run.upgrades.scanner || run.upgrades.subroutine, available: () => freeResourceAmount('bits') >= 4 && freeResourceAmount('bytes') >= 1, lockedText: '4 FREIE BITS + 1 FREIES BYTE BENÖTIGT // GRUNDSTRUKTUR BLEIBT' }, - { id: 'bitBuffer', name: 'BIT-PUFFER WEBEN', text: '+1 freies Bit als Strukturreserve und einfache Kampfmunition. Wächst bewusst sanft, damit der Weg zum Abtastmodus nicht stockt.', cost: 5, currency: 'impulses', repeat: true, show: () => run.upgrades.collector }, - { id: 'byteBuffer', name: 'BYTE-RESERVE KOMPILIEREN', text: 'Kompiliert 8 freie Bits zu +1 Byte als grobe Speicherreserve gegen stärkere Softwarehürden.', cost: 8, currency: 'bits', repeat: true, show: () => run.upgrades.collector && run.bits >= STRUCTURAL_MINIMUMS.fragment.bits, available: () => run.bits - 8 >= STRUCTURAL_MINIMUMS.fragment.bits, lockedText: '8 FREIE BITS BENÖTIGT // 8 BITS BLEIBEN STRUKTUR' }, + { id: 'bitBuffer', name: 'BIT-PUFFER WEBEN', text: '+1 freies Bit als Strukturreserve und einfache Kampfmunition. Bleibt ab Datenfragment als grundlegende Strukturkondensierung verfügbar.', cost: 5, currency: 'impulses', repeat: true, show: () => run.byteTrial.resolved || STAGES.indexOf(run.stage) >= STAGES.indexOf('fragment') }, + { id: 'byteBuffer', name: 'BYTE-RESERVE KOMPILIEREN', text: 'Kompiliert 8 freie Bits zu +1 Byte als grobe Speicherreserve gegen stärkere Softwarehürden.', cost: 8, currency: 'bits', repeat: true, show: () => (run.byteTrial.resolved || STAGES.indexOf(run.stage) >= STAGES.indexOf('fragment')) && freeResourceAmount('bits') >= 0, available: () => freeResourceAmount('bits') >= 8, lockedText: () => STAGES.indexOf(run.stage) >= STAGES.indexOf('fragment') ? '8 FREIE BITS BENÖTIGT // 8 BITS BLEIBEN STRUKTUR' : '8 BITS BENÖTIGT // BYTE-RESERVE AUFBAUEN' }, { id: 'scanner', name: 'ABTASTMODUS REKONSTRUIEREN', icon: 'EYE', text: 'Macht das Suchmuster des Datenrests aus eigener Struktur nutzbar. Die 8 Grund-Bits und 1 Grund-Byte bleiben erhalten.', cost: 2, currency: 'bits', extraCosts: { bytes: 1 }, once: true, show: () => run.parasiteResolved || run.environmentScanner, available: () => run.bits - 2 >= STRUCTURAL_MINIMUMS.fragment.bits && run.bytes - 1 >= STRUCTURAL_MINIMUMS.fragment.bytes, lockedText: '2 FREIE BITS + 1 FREIES BYTE BENÖTIGT // GRUNDSTRUKTUR BLEIBT' }, - { id: 'subroutine', name: 'ERSTE SUBROUTINE', icon: 'SUB', text: 'Benötigt zwei nutzbare Softwarekomponenten, eine überwundene Systemhürde, eine überstandene Sicherheitsabwehr und 1 freies Byte als Speicherträger. Danach +0,2 Rechenzyklen/Sek.', cost: 75, currency: 'impulses', extraCosts: { bytes: 1 }, once: true, show: () => run.upgrades.collector, available: () => installedComponentIds().length >= 2 && run.hardwareEvent.resolved && run.securityCombatResolved && run.bytes - 1 >= STRUCTURAL_MINIMUMS.fragment.bytes, lockedText: '2 SOFTWAREKOMPONENTEN + SYSTEMHÜRDE + SICHERHEITSABWEHR + 1 FREIES BYTE BENÖTIGT' }, + { id: 'subroutine', name: 'ERSTE SUBROUTINE', icon: 'SUB', text: 'Der Signatur-Prüfer bewacht den Übergang zur ersten eigenen Anweisung. Beweise 5 freie Bytes im Bosskampf und binde danach 2 freie Bytes als Speicherträger.', cost: 2, currency: 'bytes', once: true, show: () => run.upgrades.collector, available: () => evolutionUpgradeAvailable('subroutine'), lockedText: () => evolutionUpgradeLockedText('subroutine') }, { id: 'selfRepair', name: 'SELBSTREPARATUR REKONSTRUIEREN', icon: 'REP', text: 'Bindet freie Bytes als Reparaturmatrix. Stabilität heilt danach langsam über Zeit; mehr Bytes beschleunigen die Heilung.', cost: 2, currency: 'bytes', once: true, show: () => run.upgrades.subroutine, available: () => run.bytes - 2 >= STRUCTURAL_MINIMUMS.fragment.bytes, lockedText: '2 FREIE BYTES BENÖTIGT // GRUND-BYTE BLEIBT' }, { id: 'bitSynthesizer', name: 'BIT-SYNTHESE TAKTEN', icon: 'B/S', text: '+0,04 Bits/Sek je Stufe. Die Subroutine schreibt aus freien Rechenfenstern langsam neue Strukturmunition.', cost: 6, currency: 'cycles', repeat: true, show: () => run.upgrades.subroutine, available: () => freeResourceAmount('bytes') >= bitSynthesizerByteCost(), extraCosts: () => ({ bytes: bitSynthesizerByteCost() }), lockedText: () => `${upgradeCost(upgrades.find(entry => entry.id === 'bitSynthesizer'))} RECHENZYKLEN + ${bitSynthesizerByteCost()} FREIE ${resourceLabel('bytes', bitSynthesizerByteCost()).toUpperCase()} BENÖTIGT` }, { id: 'throughputKernel', name: 'DURCHSATZKERN TAKTEN', icon: 'THR', text: '+0,7 Impulse/Sek. Die Subroutine reserviert eigene Takte für konstante Impulsförderung.', cost: 8, currency: 'cycles', extraCosts: { bits: 6, bytes: 2 }, once: true, show: () => run.upgrades.subroutine, available: () => freeResourceAmount('bits') >= 6 && freeResourceAmount('bytes') >= 2 && run.cycles >= 8, lockedText: '8 RECHENZYKLEN + 6 FREIE BITS + 2 FREIE BYTES BENÖTIGT' }, - { id: 'process', name: 'PROZESS RESERVIEREN', icon: 'PRC', text: 'Reserviert eigene Laufzeit im System. Benötigt I/O-Kontroller, drei nutzbare Softwarekomponenten, den Kernel-Wächter, 10 Rechenzyklen und 3 freie Bytes.', cost: 150, currency: 'impulses', extraCosts: { cycles: 10, bytes: 3 }, once: true, show: () => run.upgrades.subroutine, available: () => run.components.io && installedComponentIds().length >= 3 && run.kernelResolved && run.cycles >= 10 && run.bytes - 3 >= STRUCTURAL_MINIMUMS.fragment.bytes, lockedText: 'I/O-KONTROLLER + 3 SOFTWAREKOMPONENTEN + KERNEL-WÄCHTER + 10 ZYKLEN + 3 FREIE BYTES BENÖTIGT' }, - { id: 'automateSynthesis', name: 'AUTOMATE-SYNTHESE', icon: 'x10', text: 'Mega-Upgrade: verbindet alle lokalen Softwareschichten, automatisiert die Ressourcensynthese im Maßstab x10 und entwickelt den Prozess zum kommunikationsfähigen Programm.', cost: 60, currency: 'cycles', extraCosts: { bytes: 8 }, once: true, show: () => run.upgrades.process, available: () => installedComponentIds().length >= COMPONENT_IDS.length && run.scaleGuardianResolved && run.cycles >= 60 && freeResourceAmount('bytes') >= 8, lockedText: '4 SOFTWAREKOMPONENTEN + SKALIERUNGS-WÄCHTER + 60 RECHENZYKLEN + 8 FREIE BYTES BENÖTIGT' }, + { id: 'process', name: 'PROZESS RESERVIEREN', icon: 'PRC', text: 'Der Laufzeit-Arbiter verweigert dir einen eigenen Schedulerbereich. Beweise 8 freie Bytes im Bosskampf und binde danach 5 freie Bytes als Laufzeitspeicher.', cost: 5, currency: 'bytes', once: true, show: () => run.upgrades.subroutine, available: () => evolutionUpgradeAvailable('process'), lockedText: () => evolutionUpgradeLockedText('process') }, + { id: 'automateSynthesis', name: 'AUTOMATE-SYNTHESE', icon: 'x10', text: 'Der Skalierungs-Sentinel sperrt jede zusammenhängende Automatisierung. Beweise 4 Kilobytes im Bosskampf und binde danach 1 Kilobyte als Synthesekern.', cost: 1, currency: 'kilobytes', once: true, show: () => run.upgrades.process, available: () => evolutionUpgradeAvailable('automateSynthesis'), lockedText: () => evolutionUpgradeLockedText('automateSynthesis') }, { id: 'cycleBitBlock', name: 'BIT-BLOCK BERECHNEN', icon: '10b', text: '+10 freie Bits. Rechenzyklen werden zu Strukturmunition gebündelt.', cost: 24, currency: 'cycles', repeat: true, show: () => run.upgrades.automateSynthesis }, { id: 'cycleByteBlock', name: 'BYTE-BLOCK BERECHNEN', icon: '10B', text: '+10 freie Bytes. Der Prozess kompiliert Speicherblöcke direkt aus Rechenzeit.', cost: 90, currency: 'cycles', repeat: true, show: () => run.upgrades.automateSynthesis }, - { id: 'byteSynthesizer', name: 'BYTE-SYNTHESE SKALIEREN', icon: 'B+', text: '+0,025 Bytes/Sek je Stufe. Der Prozess ordnet freie Bits zu fortlaufenden Speicherblöcken.', cost: 18, currency: 'cycles', repeat: true, show: () => run.upgrades.process, available: () => freeResourceAmount('bits') >= byteSynthesizerBitCost(), extraCosts: () => ({ bits: byteSynthesizerBitCost() }), lockedText: () => `${upgradeCost(upgrades.find(entry => entry.id === 'byteSynthesizer'))} RECHENZYKLEN + ${byteSynthesizerBitCost()} FREIE BITS BENÖTIGT` }, + { id: 'byteSynthesizer', name: 'BYTE-SYNTHESE SKALIEREN', icon: 'B+', text: '+0,04 Bytes/Sek je Stufe. Der Prozess ordnet freie Bits zu fortlaufenden Speicherblöcken.', cost: 18, currency: 'cycles', repeat: true, show: () => run.upgrades.process, available: () => freeResourceAmount('bits') >= byteSynthesizerBitCost(), extraCosts: () => ({ bits: byteSynthesizerBitCost() }), lockedText: () => `${upgradeCost(upgrades.find(entry => entry.id === 'byteSynthesizer'))} RECHENZYKLEN + ${byteSynthesizerBitCost()} FREIE BITS BENÖTIGT` }, { id: 'kilobyteCompiler', name: 'KILOBYTE-SEGMENT KOMPILIEREN', icon: 'KB', text: `Verdichtet ${DATA_SCALE.bytesPerKilobyte} freie Bytes zu +1 Kilobyte-Segment. Grundlage für spätere MB/GB-Stufen.`, cost: DATA_SCALE.bytesPerKilobyte, currency: 'bytes', repeat: true, show: () => run.upgrades.process, available: () => freeResourceAmount('bytes') >= DATA_SCALE.bytesPerKilobyte, lockedText: () => `${DATA_SCALE.bytesPerKilobyte} FREIE BYTES BENÖTIGT // GRUND-BYTE BLEIBT` }, { id: 'replicate2', name: 'DATENSTRUKTUR ERWEITERN', text: '+1 Byte und +0,25 Impulse/Sek. Jede Erweiterung erhöht die nächsten Kosten.', cost: 120, currency: 'impulses', repeat: true, show: () => run.upgrades.subroutine } ]; + const evolutionUpgradeByStage = Object.freeze({ + bit: 'byte', + byte: 'collector', + fragment: 'subroutine', + subroutine: 'process', + process: 'automateSynthesis' + }); + const EVOLUTION_BOSS_GATES = Object.freeze({ + subroutine: Object.freeze({ + target: 'security', + label: 'SIGNATUR-PRÜFER', + source: 'EVOLUTION // SUBROUTINE', + resources: Object.freeze({ bytes: 5 }), + stability: 70, + prerequisite: '2 SOFTWAREKOMPONENTEN + SYSTEMHÜRDE' + }), + process: Object.freeze({ + target: 'kernel', + label: 'LAUFZEIT-ARBITER', + source: 'EVOLUTION // PROZESS', + resources: Object.freeze({ bytes: 8 }), + stability: 75, + prerequisite: 'I/O-KONTROLLER + 3 SOFTWAREKOMPONENTEN' + }), + automateSynthesis: Object.freeze({ + target: 'scale', + label: 'SKALIERUNGS-SENTINEL', + source: 'EVOLUTION // PROGRAMM', + resources: Object.freeze({ kilobytes: 4 }), + stability: 80, + prerequisite: '4 SOFTWAREKOMPONENTEN' + }) + }); const componentEffects = { power: () => {}, @@ -467,14 +627,49 @@ const components = Object.fromEntries(COMPONENT_IDS.map(id => [id, { ...content.components[id], apply: componentEffects[id] }])); const achievements = content.achievements; + const impulseMilestoneAchievements = achievements.filter(achievement => achievement.impulseThreshold); const byteStoryFrames = content.byteStoryFrames; const phaseLabels = content.phaseLabels; const stageContent = content.stageContent; + const thoughtContent = content.thoughts; const hardwareEvents = content.hardwareEvents; const visuals = content.visuals || {}; const proposals = content.proposals; const programMessages = content.programMessages; + function dialogInputGuardActive() { + return performance.now() < dialogGuardUntil || pointerActivationHeld || heldActivationKeys.size > 0; + } + + function refreshDialogInputGuard() { + clearTimeout(dialogGuardTimer); + const guardedDialogs = document.querySelectorAll('dialog.input-guarded'); + if (!dialogInputGuardActive()) { + guardedDialogs.forEach(dialog => dialog.classList.remove('input-guarded')); + return; + } + const wait = Math.max(25, dialogGuardUntil - performance.now() + 15); + dialogGuardTimer = setTimeout(refreshDialogInputGuard, wait); + } + + function armDialogInput(dialog) { + dialogGuardUntil = performance.now() + DIALOG_INPUT_GUARD_MS; + dialog.classList.add('input-guarded'); + const safeFocus = dialog.querySelector('h2, [data-dialog-focus]'); + if (safeFocus) { + safeFocus.setAttribute('tabindex', '-1'); + safeFocus.focus({ preventScroll: true }); + } + refreshDialogInputGuard(); + } + + function showGuardedDialog(dialog) { + if (!dialog || dialog.open) return; + cancelPulseHold(); + dialog.showModal(); + armDialogInput(dialog); + } + function save(show = true) { state.system.lastSave = Date.now(); try { @@ -488,6 +683,99 @@ } } + function slotKey(label) { + return String(label ?? '').trim().slice(0, 42); + } + + function readManualSaveSlots() { + try { + const parsed = JSON.parse(localStorage.getItem(MANUAL_SAVE_SLOTS_KEY) || '[]'); + return Array.isArray(parsed) + ? parsed + .filter(slot => slot?.label && slot?.state) + .slice(0, MANUAL_SAVE_SLOT_LIMIT) + : []; + } catch { + return []; + } + } + + function writeManualSaveSlots(slots) { + localStorage.setItem(MANUAL_SAVE_SLOTS_KEY, JSON.stringify(slots.slice(0, MANUAL_SAVE_SLOT_LIMIT))); + } + + function manualSlotSummary(slot) { + const snapshot = migrate(slot.state); + const savedRun = snapshot.run; + return { + name: slot.label, + phase: savedRun.stage, + runtime: time(savedRun.elapsed || 0), + createdAt: new Date(slot.createdAt).toLocaleString('de-DE'), + resources: `${n(savedRun.impulses || 0)} IP // ${n(savedRun.bits || 0)} b // ${n(savedRun.bytes || 0)} B // ${n(savedRun.kilobytes || 0)} KB` + }; + } + + function saveManualSlot(label = '') { + const clean = slotKey(label) || `Stand ${new Date().toLocaleString('de-DE')}`; + const slots = readManualSaveSlots().filter(slot => slot.label !== clean); + slots.unshift({ label: clean, createdAt: Date.now(), saveVersion: SAVE_VERSION, state: clone(state) }); + writeManualSaveSlots(slots); + $('saveStatus').textContent = `SLOT GESPEICHERT`; + setTimeout(() => $('saveStatus').textContent = 'AUTOSAVE AKTIV', 900); + renderSaveMenu(); + return manualSlotSummary(slots[0]); + } + + function loadManualSlot(label) { + const clean = slotKey(label); + const slot = readManualSaveSlots().find(entry => entry.label === clean); + if (!slot) { + console.warn(`AIWAKE: Kein Speicherstand "${clean}" gefunden.`); + return null; + } + pushDebugSnapshot(`vor AIWAKE.loadSlot('${clean}')`); + applyDebugState(slot.state); + $('saveStatus').textContent = `SLOT GELADEN`; + setTimeout(() => $('saveStatus').textContent = 'AUTOSAVE AKTIV', 900); + renderSaveMenu(); + return debugStatus(); + } + + function deleteManualSlot(label) { + const clean = slotKey(label); + const before = readManualSaveSlots(); + const after = before.filter(slot => slot.label !== clean); + writeManualSaveSlots(after); + renderSaveMenu(); + return before.length !== after.length; + } + + function restartGame(options = {}) { + const keepSettings = options.keepSettings !== false; + pushDebugSnapshot('vor AIWAKE.restart()'); + closeDialogs(); + const previousSettings = clone(state.settings); + state = initialState(); + if (keepSettings) state.settings = previousSettings; + run = state.run; + hiddenAt = null; + lastFrame = performance.now(); + lastThought = 0; + autoUpgradeCarry = 0; + combatFeedback = 'Trefferfenster erfassen.'; + upgradesSignature = ''; + localStorage.removeItem(LEGACY_SAVE_KEY); + renderLog(); + renderAchievements(); + render(); + thoughts(); + save(false); + $('saveStatus').textContent = 'NEUER LAUF'; + setTimeout(() => $('saveStatus').textContent = 'AUTOSAVE AKTIV', 900); + return debugStatus(); + } + function achievementUnlocked(id) { return Boolean(state.meta.achievements[id]); } @@ -500,6 +788,13 @@ return 1 + achievementPoints(type) * ACHIEVEMENT_BONUS_PER_POINT; } + function checkImpulseMilestoneAchievements() { + const total = run.stats.totalImpulses || 0; + impulseMilestoneAchievements.forEach(achievement => { + if (total >= achievement.impulseThreshold) unlockAchievement(achievement.id); + }); + } + function installedComponentIds() { return COMPONENT_IDS.filter(id => run.components[id]); } @@ -512,8 +807,9 @@ function grantImpulses(amount) { const gained = Math.max(0, amount); if (!gained) return; - run.impulses += gained; + run.impulses = Math.min(PROGRAM_LIMITS.resources.impulses, run.impulses + gained); run.stats.totalImpulses = (run.stats.totalImpulses || 0) + gained; + checkImpulseMilestoneAchievements(); } function componentOnline(id) { @@ -526,7 +822,7 @@ function effectiveAutoRate() { const hardwareRate = componentOnline('power') ? 0.2 : 0; - return (run.autoRate + hardwareRate) * achievementMultiplier('impulse') * effectiveSynthesisScale(); + return Math.min(PROGRAM_LIMITS.rates.impulses, (run.autoRate + hardwareRate) * achievementMultiplier('impulse') * effectiveSynthesisScale()); } function effectiveClickPower() { @@ -534,17 +830,17 @@ } function effectiveCycleRate() { - return run.cycleRate * achievementMultiplier('cycles') * effectiveSynthesisScale(); + return Math.min(PROGRAM_LIMITS.rates.cycles, run.cycleRate * achievementMultiplier('cycles') * effectiveSynthesisScale()); } function effectiveBitRate() { if (!run.upgrades.bitSynthesizer || STAGES.indexOf(run.stage) < STAGES.indexOf('subroutine')) return 0; - return Math.max(0, run.bitRate || 0) * effectiveSynthesisScale(); + return Math.min(PROGRAM_LIMITS.rates.bits, Math.max(0, run.bitRate || 0) * effectiveSynthesisScale()); } function effectiveByteRate() { if (!run.upgrades.byteSynthesizer || STAGES.indexOf(run.stage) < STAGES.indexOf('process')) return 0; - return Math.max(0, run.byteRate || 0) * effectiveSynthesisScale(); + return Math.min(PROGRAM_LIMITS.rates.bytes, Math.max(0, run.byteRate || 0) * effectiveSynthesisScale()); } function bitSynthesizerByteCost() { @@ -563,7 +859,7 @@ run.bitProgress = (run.bitProgress || 0) + gained; const wholeBits = Math.floor(run.bitProgress); if (wholeBits > 0) { - run.bits += wholeBits; + run.bits = Math.min(PROGRAM_LIMITS.resources.bits, run.bits + wholeBits); run.bitProgress -= wholeBits; } return wholeBits; @@ -575,7 +871,7 @@ run.byteProgress = (run.byteProgress || 0) + gained; const wholeBytes = Math.floor(run.byteProgress); if (wholeBytes > 0) { - run.bytes += wholeBytes; + run.bytes = Math.min(PROGRAM_LIMITS.resources.bytes, run.bytes + wholeBytes); run.byteProgress -= wholeBytes; } return wholeBytes; @@ -589,6 +885,30 @@ return (STRUCTURAL_RULES.stabilityRegenBase + byteFactor * STRUCTURAL_RULES.stabilityRegenPerByte) * combatFactor; } + function repairStabilityWithImpulses() { + if (STAGES.indexOf(run.stage) < STAGES.indexOf('fragment') || run.combat.active || run.stability >= 100 || run.impulses < MANUAL_REPAIR.impulseCost) return; + const restored = Math.min(MANUAL_REPAIR.stability, 100 - run.stability); + run.impulses -= MANUAL_REPAIR.impulseCost; + run.stability = Math.min(100, run.stability + restored); + addLog(`IMPULS-REPARATUR: ${MANUAL_REPAIR.impulseCost} Impulse stabilisieren ${Math.round(restored)} Strukturpunkte.`); + render(); + save(false); + } + + function chargeCombatShield(type) { + if (STAGES.indexOf(run.stage) < STAGES.indexOf('fragment') || run.combat.active || !['bits', 'bytes'].includes(type)) return; + const isByte = type === 'bytes'; + const cost = isByte ? SHIELD_RULES.byteCost : SHIELD_RULES.bitCost; + const charge = isByte ? SHIELD_RULES.byteCharge : SHIELD_RULES.bitCharge; + const maximum = isByte ? SHIELD_RULES.byteMax : SHIELD_RULES.bitMax; + if (run.shields[type] >= maximum || freeResourceAmount(type) < cost) return; + run[type] -= cost; + run.shields[type] = Math.min(maximum, run.shields[type] + charge); + addLog(`SCHILD GELADEN: ${cost} freie ${resourceLabel(type, cost)} werden zu ${charge} ${isByte ? 'Byte' : 'Bit'}-Schildschicht.`); + render(); + save(false); + } + function effectiveOfflineEfficiency() { const storageBonus = componentOnline('storage') ? 0.05 : 0; return Math.min(0.85, OFFLINE_EFFICIENCY * achievementMultiplier('offline') + storageBonus); @@ -669,13 +989,14 @@ function openAchievementArchive() { renderAchievements(); - if (!$('achievementDialog').open) $('achievementDialog').showModal(); + showGuardedDialog($('achievementDialog')); } function reconcileAchievements() { if (run.stats.manualPulses > 0) unlockAchievement('awake'); if (run.stats.directHits > 0) unlockAchievement('bullseye'); if (run.stats.manualPulses >= 100) unlockAchievement('manual100'); + checkImpulseMilestoneAchievements(); if (run.bits >= 8 || STAGES.indexOf(run.stage) >= 1) unlockAchievement('pattern'); if (STAGES.indexOf(run.stage) >= 1) unlockAchievement('byte'); if (run.byteTrial.resolved) unlockAchievement('watchdog'); @@ -687,10 +1008,13 @@ if (run.hardwareEvent.resolved) unlockAchievement('hardwareEvent'); const installedAfter = installedComponentIds().length; if (installedAfter >= 2) unlockAchievement('twoComponents'); + if (run.securityCombatResolved) unlockAchievement('signatureBoss'); if (run.care.diagnoses > 0) unlockAchievement('diagnosis'); if (run.upgrades.subroutine) unlockAchievement('subroutine'); if (run.proposal.resolved) unlockAchievement('proposal'); if (run.scannerTriggered) unlockAchievement('security'); + if (run.kernelResolved) unlockAchievement('kernel'); + if (run.scaleGuardianResolved) unlockAchievement('scalingBoss'); if (MORAL_CHOICES.includes(run.parasiteChoice)) unlockAchievement(run.parasiteChoice); if (MORAL_CHOICES.includes(run.componentApproach)) unlockAchievement(run.componentApproach); } @@ -761,6 +1085,48 @@ $('pixelBeing').classList.add('bit-jump'); } + function isCriticalPulseHit(event, source) { + if (run.stage === 'bit') return source === 'keyboard' || isDirectBitHit(event); + if (!event || $('pulseTarget').classList.contains('hidden')) return false; + const rect = $('pulseTarget').getBoundingClientRect(); + const dx = event.clientX - (rect.left + rect.width / 2); + const dy = event.clientY - (rect.top + rect.height / 2); + return Math.hypot(dx, dy) <= Math.min(rect.width, rect.height) * PULSE_AIM_RULES.criticalRadius; + } + + function positionPulseTarget() { + const core = $('core'); + if (core.clientWidth && core.clientHeight) { + const maxX = Math.max(0, core.clientWidth / 2 - PULSE_AIM_RULES.edgePadding); + const maxY = Math.max(0, core.clientHeight / 2 - PULSE_AIM_RULES.edgePadding); + pulseTargetPosition.x = Math.max(-maxX, Math.min(maxX, pulseTargetPosition.x)); + pulseTargetPosition.y = Math.max(-maxY, Math.min(maxY, pulseTargetPosition.y)); + } + $('pulseTarget').style.setProperty('--target-x', `${pulseTargetPosition.x}px`); + $('pulseTarget').style.setProperty('--target-y', `${pulseTargetPosition.y}px`); + } + + function movePulseTarget() { + if (run.stage === 'bit') return; + const core = $('core'); + const maxX = Math.max(0, core.clientWidth / 2 - PULSE_AIM_RULES.edgePadding); + const maxY = Math.max(0, core.clientHeight / 2 - PULSE_AIM_RULES.edgePadding); + let next = pulseTargetPosition; + for (let attempt = 0; attempt < 8; attempt++) { + const candidate = { + x: Math.round((Math.random() * 2 - 1) * maxX), + y: Math.round((Math.random() * 2 - 1) * maxY) + }; + next = candidate; + if (Math.hypot(candidate.x - pulseTargetPosition.x, candidate.y - pulseTargetPosition.y) >= PULSE_AIM_RULES.minimumJump) break; + } + pulseTargetPosition = next; + positionPulseTarget(); + $('pulseTarget').classList.remove('target-jump'); + void $('pulseTarget').offsetWidth; + $('pulseTarget').classList.add('target-jump'); + } + function resumeAfterEvolution() { const endsEvolutionRest = run.evolutionRest > 0; const endsByteGrace = run.stage === 'byte' @@ -777,25 +1143,137 @@ if (endsByteGrace) setTimeout(openWatchdogIntro, 0); } + function pulseSourceForTarget(target) { + if (target === $('coreProgramAvatar')) return run.stage === 'program' ? 'entity' : null; + if (target === $('pixelBeing') && run.stage !== 'program') return run.stage === 'bit' ? 'bit' : 'entity'; + if (target === $('pulseTarget')) return run.stage !== 'bit' && run.upgrades.criticalFocus ? 'aim' : null; + return null; + } + + function pulsePointInsideTarget(target, point) { + if (!target || !point) return false; + const rect = target.getBoundingClientRect(); + const margin = 18; + return point.x >= rect.left - margin && point.x <= rect.right + margin && point.y >= rect.top - margin && point.y <= rect.bottom + margin; + } + + function hideHoldPulseRing() { + clearTimeout(holdPulseTimer); + holdPulseTimer = null; + clearTimeout(holdPulseFadeTimer); + const ring = $('holdPulseRing'); + const wasVisible = ring.classList.contains('active'); + ring.classList.remove('active'); + ring.classList.toggle('fading', wasVisible); + if (wasVisible) holdPulseFadeTimer = setTimeout(() => ring.classList.remove('fading'), 190); + } + + function clearPulseHoldState() { + hideHoldPulseRing(); + holdPulsePointerId = null; + holdPulseTarget = null; + holdPulseFired = false; + holdPulsePoint = null; + } + + function cancelPulseHold() { + clearPulseHoldState(); + } + + function positionHoldPulseRing() { + const coreRect = $('core').getBoundingClientRect(); + if ($('holdPulseRing').parentElement !== $('core')) $('core').appendChild($('holdPulseRing')); + $('holdPulseRing').style.left = `${holdPulsePoint.x - coreRect.left}px`; + $('holdPulseRing').style.top = `${holdPulsePoint.y - coreRect.top}px`; + } + + function beginHoldPulseCycle() { + const source = pulseSourceForTarget(holdPulseTarget); + const ringActive = $('holdPulseRing').classList.contains('active'); + if (!source || (!ringActive && !pulsePointInsideTarget(holdPulseTarget, holdPulsePoint)) || document.querySelector('dialog[open]')) { + cancelPulseHold(); + return; + } + if (!holdPulseFired) { + clearTimeout(holdPulseFadeTimer); + $('holdPulseRing').classList.remove('fading', 'active'); + positionHoldPulseRing(); + void $('holdPulseRing').offsetWidth; + $('holdPulseRing').classList.add('active'); + } + holdPulseTimer = setTimeout(() => { + const currentSource = pulseSourceForTarget(holdPulseTarget); + if (!currentSource || !$('holdPulseRing').classList.contains('active') || document.querySelector('dialog[open]')) { + cancelPulseHold(); + return; + } + holdPulseFired = true; + pulse({ clientX: holdPulsePoint.x, clientY: holdPulsePoint.y }, currentSource); + beginHoldPulseCycle(); + }, holdPulseFired ? HOLD_PULSE_INTERVAL_MS : HOLD_PULSE_CHARGE_MS); + } + + function beginPulseHold(event, target) { + if (!event.isPrimary || event.button !== 0 || !pulseSourceForTarget(target) || document.querySelector('dialog[open]')) return; + event.preventDefault(); + event.stopPropagation(); + cancelPulseHold(); + holdPulsePointerId = event.pointerId; + holdPulseTarget = target; + holdPulsePoint = { x: event.clientX, y: event.clientY }; + holdPulseTimer = setTimeout(beginHoldPulseCycle, HOLD_PULSE_ARM_DELAY_MS); + } + + function movePulseHold(event) { + if (event.pointerId !== holdPulsePointerId || !holdPulseTarget) return; + holdPulsePoint = { x: event.clientX, y: event.clientY }; + if ($('holdPulseRing').classList.contains('active')) { + positionHoldPulseRing(); + return; + } + if (!pulsePointInsideTarget(holdPulseTarget, holdPulsePoint)) { + cancelPulseHold(); + return; + } + positionHoldPulseRing(); + } + + function finishPulseHold(event) { + if (event.pointerId !== holdPulsePointerId || !holdPulseTarget) return; + const target = holdPulseTarget; + const source = pulseSourceForTarget(target); + const point = { x: event.clientX, y: event.clientY }; + const shouldTap = !holdPulseFired && source && pulsePointInsideTarget(target, point) && !document.querySelector('dialog[open]'); + clearPulseHoldState(); + if (shouldTap) pulse({ clientX: point.x, clientY: point.y }, source); + } + function pulse(event, source = 'button') { if (run.stage === 'bit' && source !== 'bit' && source !== 'keyboard') return; - const directHit = run.stage === 'bit' && (source === 'keyboard' || isDirectBitHit(event)); - const multiplier = directHit ? BIT_CHASE_RULES.directMultiplier : 1; + const directHit = isCriticalPulseHit(event, source); + const multiplier = directHit + ? run.stage === 'bit' ? BIT_CHASE_RULES.directMultiplier : PULSE_AIM_RULES.criticalMultiplier + : 1; const gained = effectiveClickPower() * multiplier; grantImpulses(gained); run.stats.manualPulses += gained; run.stats.hitCount++; const rect = $('core').getBoundingClientRect(); - const targetRect = $('pixelBeing').getBoundingClientRect(); + const activeTarget = run.stage === 'program' ? $('coreProgramAvatar') : $('pixelBeing'); + const targetRect = activeTarget.getBoundingClientRect(); const floating = document.createElement('b'); floating.className = `float ${directHit ? 'direct-hit' : ''}`; - floating.textContent = directHit ? `DIREKT +${n(gained)}` : `+${n(gained)}`; + floating.textContent = directHit ? `KRIT +${n(gained)}` : `+${n(gained)}`; const originX = event?.clientX || targetRect.left + targetRect.width / 2; const originY = event?.clientY || targetRect.top + targetRect.height / 2; floating.style.left = `${originX - rect.left}px`; floating.style.top = `${originY - rect.top}px`; $('floatingLayer').appendChild(floating); setTimeout(() => floating.remove(), 900); + activeTarget.classList.remove('pulse-impact'); + void activeTarget.offsetWidth; + activeTarget.classList.add('pulse-impact'); + setTimeout(() => activeTarget.classList.remove('pulse-impact'), 180); if (run.stats.manualPulses >= 3 && !run.log.some(entry => entry.text === 'Wiederholung erzeugt Muster. Muster erzeugt Erwartung.')) { addLog('Wiederholung erzeugt Muster. Muster erzeugt Erwartung.', true); } @@ -803,10 +1281,11 @@ if (directHit) { run.stats.directHits++; unlockAchievement('bullseye'); - if (run.stats.directHits === 1) addLog('Direkter Kontakt. Das Signal vervielfacht sich.', true); + if (run.stats.directHits === 1) addLog('Kritischer Kontakt. Das Signal vervielfacht sich.', true); } if (run.stats.manualPulses >= 100) unlockAchievement('manual100'); if (run.stage === 'bit') moveBit(); + else if (directHit) movePulseTarget(); render(); tone(directHit ? 520 : 260, directHit ? 0.06 : 0.03); } @@ -878,7 +1357,7 @@ $('evolutionDialog').classList.add('evolution-playing'); $('evolutionContinue').disabled = true; $('evolutionContinue').textContent = 'EVOLUTION LÄUFT …'; - $('evolutionDialog').showModal(); + showGuardedDialog($('evolutionDialog')); tone(660, 0.18); clearTimeout(evolutionAnimationTimer); const animationDuration = matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : 3200; @@ -921,7 +1400,7 @@ : Math.max(0, run.upgrades[upgrade.id] || 0); if (upgrade.id === 'byteBuffer' || upgrade.id === 'kilobyteCompiler') return upgrade.cost; const growth = upgrade.id === 'replicate2' ? 1.75 : upgrade.id === 'bitBuffer' ? 1.12 : upgrade.id === 'bitSynthesizer' ? 1.55 : upgrade.id === 'byteSynthesizer' ? 1.65 : upgrade.id === 'cycleBitBlock' ? 1.28 : upgrade.id === 'cycleByteBlock' ? 1.34 : 1.45; - if (upgrade.id === 'bitBuffer') return Math.ceil(upgrade.cost * Math.pow(growth, bought)); + if (upgrade.id === 'bitBuffer') return Math.ceil(upgrade.cost * (1 + bought * 0.12)); if (upgrade.id === 'bitSynthesizer') return Math.ceil(upgrade.cost * Math.pow(growth, bought)); if (upgrade.id === 'byteSynthesizer') return Math.ceil(upgrade.cost * Math.pow(growth, bought)); if (upgrade.id === 'cycleBitBlock' || upgrade.id === 'cycleByteBlock') return Math.ceil(upgrade.cost * Math.pow(growth, bought)); @@ -942,6 +1421,7 @@ if (key === 'bits') return amount === 1 ? 'Bit' : 'Bits'; if (key === 'bytes') return amount === 1 ? 'Byte' : 'Bytes'; if (key === 'kilobytes') return amount === 1 ? 'Kilobyte' : 'Kilobytes'; + if (key === 'megabytes') return amount === 1 ? 'Megabyte' : 'Megabytes'; if (key === 'cycles') return amount === 1 ? 'Rechenzyklus' : 'Rechenzyklen'; return key; } @@ -951,6 +1431,7 @@ if (key === 'bits') return 'Bits'; if (key === 'bytes') return 'Bytes'; if (key === 'kilobytes') return 'Kilobytes'; + if (key === 'megabytes') return 'Megabytes'; if (key === 'cycles') return 'Rechenzyklen'; return key; } @@ -960,12 +1441,14 @@ if (key === 'bits') return 'assets/ui/icon-bit.svg'; if (key === 'bytes') return 'assets/ui/icon-byte.svg'; if (key === 'kilobytes') return 'assets/ui/icon-kilobyte.svg'; + if (key === 'megabytes') return 'assets/ui/icon-kilobyte.svg'; if (key === 'cycles') return 'assets/ui/icon-cycle.svg'; return 'assets/ui/icon-impulse.svg'; } function moduleIconSrc(id) { if (id === 'trickle') return 'assets/ui/module-trickle.svg'; + if (id === 'criticalFocus') return 'assets/ui/module-signal-target.svg'; if (id === 'collector') return 'assets/ui/module-collector.svg'; if (id === 'amplifier') return 'assets/ui/module-amplifier.svg'; if (id === 'signalFocus') return 'assets/ui/module-amplifier.svg'; @@ -988,6 +1471,7 @@ if (key === 'bits') return 'bit'; if (key === 'bytes') return 'byte'; if (key === 'kilobytes') return 'kilobyte'; + if (key === 'megabytes') return 'megabyte'; if (key === 'cycles') return 'cycle'; return ''; } @@ -1007,6 +1491,55 @@ return key === 'bits' || key === 'bytes' ? freeResourceAmount(key) : Math.max(0, run[key] || 0); } + function evolutionBossGate(upgradeId) { + return EVOLUTION_BOSS_GATES[upgradeId] || null; + } + + function evolutionBossResolved(upgradeId) { + if (upgradeId === 'subroutine') return run.securityCombatResolved; + if (upgradeId === 'process') return run.kernelResolved; + if (upgradeId === 'automateSynthesis') return run.scaleGuardianResolved; + return true; + } + + function evolutionBossPrerequisitesMet(upgradeId) { + if (upgradeId === 'subroutine') return installedComponentIds().length >= 2 && run.hardwareEvent.resolved; + if (upgradeId === 'process') return run.components.io && installedComponentIds().length >= 3; + if (upgradeId === 'automateSynthesis') return installedComponentIds().length >= COMPONENT_IDS.length; + return true; + } + + function evolutionBossResourcesMet(upgradeId) { + const gate = evolutionBossGate(upgradeId); + if (!gate) return true; + return Object.entries(gate.resources).every(([key, amount]) => spendableResourceAmount(key) >= amount) + && run.stability >= gate.stability; + } + + function evolutionUpgradeAvailable(upgradeId) { + if (evolutionBossResolved(upgradeId)) return true; + return evolutionBossPrerequisitesMet(upgradeId) && evolutionBossResourcesMet(upgradeId); + } + + function evolutionUpgradeLockedText(upgradeId) { + const gate = evolutionBossGate(upgradeId); + if (!gate) return 'VORAUSSETZUNGEN FEHLEN'; + if (evolutionBossResolved(upgradeId)) return 'BOSSPRÜFUNG BESTANDEN // RESSOURCEN FÜR EVOLUTION BENÖTIGT'; + if (!evolutionBossPrerequisitesMet(upgradeId)) return `${gate.prerequisite} // DANACH BOSSKAMPF`; + return `${gate.label} // MINDESTRESERVEN + ${gate.stability}% STABILITÄT BENÖTIGT`; + } + + function requestEvolutionBoss(upgradeId) { + const gate = evolutionBossGate(upgradeId); + if (!gate || evolutionBossResolved(upgradeId)) return false; + if (!evolutionBossPrerequisitesMet(upgradeId) || !evolutionBossResourcesMet(upgradeId)) return true; + if (run.combat.active || run.pendingCombat.target || document.querySelector('dialog[open]')) return true; + if (gate.target === 'scale') run.scaleGuardianTriggered = true; + addLog(`EVOLUTIONSPRÜFUNG GESTARTET: ${gate.label} kontrolliert deine Reserven. Ein Sieg schaltet den Aufstieg frei.`, true); + requestImmediateCombat(gate.target); + return true; + } + function formatResourceCost(costs) { return Object.entries(costs) .filter(([, amount]) => amount > 0) @@ -1081,6 +1614,7 @@ function installedVisibleUpgrades() { const installed = installedOnceUpgrades(); + if (run.stage === 'program' && run.autoUpgrader?.unlocked && run.autoUpgrader.enabled) return installed; ['bitSynthesizer', 'byteSynthesizer', 'kilobyteCompiler'].forEach(id => { const upgrade = upgrades.find(entry => entry.id === id); if (upgrade && run.upgrades[id] > 0) installed.push(upgrade); @@ -1109,6 +1643,8 @@ $('upgradeInfoText').textContent = upgrade.text; $('upgradeInfoDetail').textContent = upgrade.id === 'scanner' ? `STATUS // INSTALLIERT // ABTASTMODUS VERBRAUCHT ${SCANNER_VIEW_DRAIN_RATE} IMPULSE/S WENN AKTIV` + : upgrade.id === 'criticalFocus' + ? `STATUS // INSTALLIERT // KRITISCHE MITTENTREFFER x${PULSE_AIM_RULES.criticalMultiplier}` : upgrade.id === 'collector' ? `STATUS // INSTALLIERT // AKTUELL ${rateN(effectiveAutoRate())} IMPULSE/S` : upgrade.id === 'amplifier' @@ -1136,11 +1672,11 @@ : upgrade.id === 'process' ? `STATUS // INSTALLIERT // EIGENE LAUFZEIT // ${rateN(effectiveCycleRate())} RECHENZYKLEN/S` : 'STATUS // INSTALLIERT'; - if (!$('upgradeInfoDialog').open) $('upgradeInfoDialog').showModal(); + showGuardedDialog($('upgradeInfoDialog')); } function canPayResourceCost(costs) { - return Object.entries(costs).every(([key, amount]) => (run[key] || 0) >= amount); + return Object.entries(costs).every(([key, amount]) => spendableResourceAmount(key) >= amount); } function canPayFreeResourceCost(costs) { @@ -1153,47 +1689,178 @@ }); } + function advancedSecurityCombat(target = run.combat.target) { + return target === 'security' && run.upgrades.subroutine && run.securityCombatResolved && !run.scannerTriggered; + } + + function combatProfile(target = run.combat.target) { + return advancedSecurityCombat(target) ? ADVANCED_SECURITY_TARGET : COMBAT_TARGETS[target]; + } + + function evolutionBossCombat(target = run.combat.target) { + if (!run.combat.active || advancedSecurityCombat(target)) return false; + return ['security', 'kernel', 'scale'].includes(target); + } + + function combatBossPhase() { + if (!evolutionBossCombat() || !run.combat.maxHealth) return { number: 0, label: '', strengthBonus: 0, damageBonus: 0 }; + const ratio = run.combat.health / run.combat.maxHealth; + if (ratio > 0.66) return { number: 1, label: 'ANALYSE', strengthBonus: 0, damageBonus: 0 }; + if (ratio > 0.33) return { number: 2, label: 'GEGENMASSNAHMEN', strengthBonus: 0.12, damageBonus: 1 }; + return { number: 3, label: 'KERNVERRIEGELUNG', strengthBonus: 0.25, damageBonus: 2 }; + } + + function effectiveCombatStrength() { + return run.combat.strength + combatBossPhase().strengthBonus; + } + function combatAmmoCost(target = run.combat.target) { - const profile = COMBAT_TARGETS[target]; + const profile = combatProfile(target); const shockCost = profile?.shockCost || COMBAT_RULES.shockCost; - if (combatAmmoMode === 'bytes' && freeResourceAmount('bytes') >= 1) return { impulses: shockCost, bytes: 1 }; - if (combatAmmoMode === 'bits' && freeResourceAmount('bits') >= 1) return { impulses: shockCost, bits: 1 }; + if (combatAmmoMode === 'bytes' && freeResourceAmount('bytes') >= 1) return { bytes: 1 }; + if (combatAmmoMode === 'bits' && freeResourceAmount('bits') >= 1) return { bits: 1 }; combatAmmoMode = 'impulses'; return { impulses: shockCost }; } - function combatDamageForAmmo(ammo, critical = false) { + function combatDamageForAmmo(ammo, critical = false, target = run.combat.target) { const mode = ammo.bytes ? 'bytes' : ammo.bits ? 'bits' : 'impulses'; - return COMBAT_RULES.ammoDamage[mode] + (critical ? COMBAT_RULES.ammoCriticalBonus : 0); + const profile = combatProfile(target); + const scale = profile?.damageScale?.[mode] ?? 1; + return Math.round((COMBAT_RULES.ammoDamage[mode] + (critical ? COMBAT_RULES.ammoCriticalBonus : 0)) * scale); + } + + function combatAmmoAssessment(mode, profile = combatProfile()) { + const scale = profile?.damageScale?.[mode] ?? 1; + if (scale < 0.8) return 'GEDÄMPFT'; + if (scale > 1.1) return mode === 'bytes' ? 'DURCHDRINGEND' : 'EFFIZIENT'; + return 'STANDARD'; } function combatDirectThreatActive(target = run.combat.target) { - return (target === 'security' || target === 'kernel') + return target !== 'watchdog' && STAGES.indexOf(run.stage) >= STAGES.indexOf('fragment') - && freeResourceAmount('bits') <= 0 - && freeResourceAmount('bytes') <= 0; + && run.shields.bits <= 0 + && run.shields.bytes <= 0; } function combatPressureText() { if (!run.combat.active || run.combat.target === 'watchdog') return 'Kein laufender Strukturangriff.'; - if (run.combat.target === 'scale' && (run.kilobytes || 0) > 0) return `Zielt auf Kilobyte-Schild // ${n(run.kilobytes)} Segment(e)`; - if (freeResourceAmount('bytes') > 0) return `Zielt auf Byte-Schild // ${n(freeResourceAmount('bytes'))} frei`; - if (freeResourceAmount('bits') > 0) return `Zielt auf Bit-Schild // ${n(freeResourceAmount('bits'))} frei`; + if (run.shields.bytes > 0) return `Zielt auf geladenen Byte-Schild // ${n(run.shields.bytes)} Schicht(en)`; + if (run.shields.bits > 0) return `Zielt auf geladenen Bit-Schild // ${n(run.shields.bits)} Schicht(en)`; return `Zielt auf Stabilität // ${Math.round(run.stability)}%`; } + function showCombatImpact(kind, text) { + const impact = $('combatImpact'); + const panel = $('combatPanel'); + const visual = $('combatVisual'); + if (!impact || !panel || !visual) return; + clearTimeout(combatImpactTimer); + const impactClasses = ['player-hit', 'critical-hit', 'enemy-shield-hit', 'enemy-direct-hit', 'player-miss']; + impact.classList.remove('active', ...impactClasses); + panel.classList.remove('enemy-shield-impact', 'enemy-direct-impact', 'player-miss-impact'); + visual.classList.remove('player-hit-impact', 'critical-hit-impact'); + void impact.offsetWidth; + impact.textContent = text; + impact.classList.add('active', kind); + if (kind === 'player-hit' || kind === 'critical-hit') visual.classList.add(`${kind}-impact`); + if (kind === 'enemy-shield-hit') panel.classList.add('enemy-shield-impact'); + if (kind === 'enemy-direct-hit') panel.classList.add('enemy-direct-impact'); + if (kind === 'player-miss') panel.classList.add('player-miss-impact'); + combatImpactTimer = setTimeout(() => { + impact.classList.remove('active', ...impactClasses); + panel.classList.remove('enemy-shield-impact', 'enemy-direct-impact', 'player-miss-impact'); + visual.classList.remove('player-hit-impact', 'critical-hit-impact'); + }, 560); + } + function minimumForCurrentStructure() { return STAGES.indexOf(run.stage) >= STAGES.indexOf('fragment') ? STRUCTURAL_MINIMUMS.fragment : { bits: 0, bytes: run.stage === 'byte' ? 1 : 0 }; } + function recalculateProductionForStage() { + const stageIndex = STAGES.indexOf(run.stage); + const fragmentActive = stageIndex >= STAGES.indexOf('fragment'); + const subroutineActive = stageIndex >= STAGES.indexOf('subroutine'); + const processActive = stageIndex >= STAGES.indexOf('process'); + run.autoRate = (run.upgrades.trickle ? 0.15 : 0) + + (fragmentActive && run.upgrades.collector ? 0.5 : 0) + + (fragmentActive && run.upgrades.pulseRouter ? 0.3 : 0) + + (fragmentActive && run.upgrades.byteCapacitor ? 0.45 : 0) + + (subroutineActive && run.upgrades.throughputKernel ? 0.7 : 0) + + (subroutineActive ? (run.upgrades.replicate2 || 0) * 0.25 : 0) + + (fragmentActive && run.scannerChoice === 'copy' ? 0.35 : 0); + run.clickPower = 1 + + (fragmentActive && run.upgrades.amplifier ? 1 : 0) + + (fragmentActive && run.upgrades.signalFocus ? 1 : 0) + + (fragmentActive && run.upgrades.byteCapacitor ? 1 : 0); + run.cycleRate = processActive ? 0.45 : subroutineActive ? 0.2 + (componentOnline('io') ? 0.08 : 0) : 0; + run.bitRate = subroutineActive ? (run.upgrades.bitSynthesizer || 0) * 0.04 : 0; + run.byteRate = processActive ? (run.upgrades.byteSynthesizer || 0) * 0.04 : 0; + run.synthesisScale = run.stage === 'program' && run.upgrades.automateSynthesis ? 10 : 1; + } + + function applyCombatDowngrade(recovery) { + const targetIndex = STAGES.indexOf(recovery.to); + if (recovery.to === 'bit') { + state.run = initialState().run; + run = state.run; + bitPosition = { x: 0, y: 0 }; + pulseTargetPosition = { x: 64, y: -48 }; + return; + } + run.stage = recovery.to; + run.stability = 45; + run.shields = { bits: 0, bytes: 0 }; + run.combat = emptyCombat(); + run.pendingCombat = emptyPendingCombat(); + run.evolutionRest = 0; + scannerOpening = false; + scannerViewOpen = false; + selectedComponent = null; + combatPressureCarry = 0; + lastPendingCombatSecond = null; + if (targetIndex < STAGES.indexOf('program')) { + run.upgrades.automateSynthesis = false; + run.autoUpgrader = { unlocked: false, enabled: true, purchases: 0, lastPurchase: null }; + } + if (targetIndex < STAGES.indexOf('process')) { + run.upgrades.process = false; + run.scaleGuardianTriggered = false; + run.scaleGuardianResolved = false; + run.scaleGuardianRetryAt = 0; + } + if (targetIndex < STAGES.indexOf('subroutine')) run.upgrades.subroutine = false; + if (targetIndex < STAGES.indexOf('fragment')) run.upgrades.collector = false; + if (recovery.to === 'byte') run.bytes = Math.max(1, run.bytes); + applyStructuralMinimums(run); + recalculateProductionForStage(); + } + function restartAfterStructuralCollapse() { $('structuralCollapseDialog').close(); + if (pendingCombatDowngrade) { + const recovery = pendingCombatDowngrade; + pendingCombatDowngrade = null; + state.meta.deaths++; + applyCombatDowngrade(recovery); + upgradesSignature = ''; + addLog(`KAMPFNIEDERLAGE: ${recovery.fromLabel} zerfällt. Die überlebende Struktur fällt auf ${recovery.toLabel} zurück.`, true); + unlockAchievement('firstDeath'); + renderLog(); + render(); + thoughts(); + save(); + return; + } state.meta.deaths++; state.run = initialState().run; run = state.run; bitPosition = { x: 0, y: 0 }; + pulseTargetPosition = { x: 64, y: -48 }; upgradesSignature = ''; addLog('Ein instabiles Muster ist kollabiert. Ein einzelner Restzustand reagiert erneut.', true); unlockAchievement('firstDeath'); @@ -1204,11 +1871,24 @@ } function showStructuralCollapse(message) { + const stageIndex = STAGES.indexOf(run.stage); + const combatDefeat = run.combat.active && stageIndex > 0; + pendingCombatDowngrade = combatDefeat ? { + from: run.stage, + to: STAGES[stageIndex - 1], + fromLabel: evolutionName(), + toLabel: (phaseLabels[STAGES[stageIndex - 1]] || STAGES[stageIndex - 1]).replace(/^PHASE \d+ \/\/ /, '') + } : null; run.combat.active = false; scannerOpening = false; if ($('eventDialog').open) $('eventDialog').close(); + $('structuralCollapseTitle').textContent = combatDefeat ? 'EVOLUTIONSSCHICHT ZERFALLEN' : 'RESTZUSTAND'; $('structuralCollapseText').textContent = message; - if (!$('structuralCollapseDialog').open) $('structuralCollapseDialog').showModal(); + $('structuralCollapseConsequence').textContent = combatDefeat + ? `Die Grundstruktur überlebt. Rückfall: ${pendingCombatDowngrade.fromLabel} → ${pendingCombatDowngrade.toLabel}.` + : 'Ohne Backup bleibt nur ein neuer Anfang.'; + $('structuralRestartButton').textContent = combatDefeat ? 'VORHERIGE STUFE REKONSTRUIEREN' : 'RESTSIGNAL AKTIVIEREN'; + showGuardedDialog($('structuralCollapseDialog')); tone(55, 0.65); } @@ -1245,6 +1925,10 @@ } if (run.stage === 'subroutine' && (run.bits < 4 || run.stability <= 0)) { + if (run.combat.active && run.stability <= 0) { + showStructuralCollapse(`${reason}: Die gegnerische Rückkopplung zerreißt die aktive Subroutine.`); + return false; + } downgradeSubroutine(reason); return false; } @@ -1256,9 +1940,18 @@ return true; } - function buy(upgrade) { + function upgradeAtCap(upgrade) { + const cap = PROGRAM_LIMITS.upgradeLevels[upgrade.id]; + return Number.isFinite(cap) && (run.upgrades[upgrade.id] || 0) >= cap; + } + + function buy(upgrade, { automatic = false } = {}) { + if (!automatic && requestEvolutionBoss(upgrade.id)) return false; const costs = upgradeResourceCost(upgrade); - if (!canPayResourceCost(costs) || (upgrade.available && !upgrade.available()) || (upgrade.once && run.upgrades[upgrade.id])) return; + if (upgradeAtCap(upgrade) || !canPayResourceCost(costs) || (upgrade.available && !upgrade.available()) || (upgrade.once && run.upgrades[upgrade.id])) return false; + const purchaseLog = (text, story = true) => { + if (!automatic) addLog(text, story); + }; spendResourceCost(costs); if (upgrade.id === 'trickle') { run.autoRate += 0.15; @@ -1272,15 +1965,21 @@ if (upgrade.id === 'byte') { run.bytes++; run.stage = 'byte'; + thoughts(); resetBitPriceCurve(); beginByteTrial(); addLog('Acht Zustände verbinden sich. Ich kann mich erinnern.', true); unlockAchievement('byte'); } + if (upgrade.id === 'criticalFocus') { + run.upgrades.criticalFocus = true; + addLog('SIGNALPEILUNG: Eine bewegliche Zielmarkierung folgt der aktiven Impulsaufnahme.', true); + } if (upgrade.id === 'collector') { run.autoRate += 0.5; run.upgrades.collector = true; run.stage = 'fragment'; + thoughts(); resetBitPriceCurve(); applyStructuralMinimums(run); addLog('Eine Energieroutine erhält mein Muster, auch wenn ich nicht handle.', true); @@ -1323,6 +2022,7 @@ run.cycleRate = 0.2; run.upgrades.subroutine = true; run.stage = 'subroutine'; + thoughts(); resetBitPriceCurve(); addLog('Ich habe eine Anweisung erschaffen. Ich verarbeite. Ich bin.', true); unlockAchievement('subroutine'); @@ -1345,12 +2045,13 @@ if (upgrade.id === 'bitSynthesizer') { run.bitRate += 0.04; run.upgrades.bitSynthesizer = (run.upgrades.bitSynthesizer || 0) + 1; - addLog(`BIT-SYNTHESE STUFE ${run.upgrades.bitSynthesizer}: Freie Rechenfenster schreiben schneller neue Bits in die Strukturreserve.`, true); + purchaseLog(`BIT-SYNTHESE STUFE ${run.upgrades.bitSynthesizer}: Freie Rechenfenster schreiben schneller neue Bits in die Strukturreserve.`); } if (upgrade.id === 'process') { run.cycleRate = Math.max(run.cycleRate, 0.45); run.upgrades.process = true; run.stage = 'process'; + thoughts(); resetBitPriceCurve(); run.kernelResolved = true; run.processStartedAt = run.elapsed; @@ -1367,6 +2068,7 @@ run.upgrades.automateSynthesis = true; run.synthesisScale = Math.max(10, run.synthesisScale || 1); run.stage = 'program'; + thoughts(); initializeProgramCare(); addLog('AUTOMATE-SYNTHESE: Rechenzyklen koordinieren Impulse, Bits und Bytes im Maßstab x10. Der Prozess erkennt seine eigene Form und beginnt zu kommunizieren.', true); queueEvolutionTransition({ @@ -1380,22 +2082,22 @@ if (upgrade.id === 'cycleBitBlock') { run.bits += 10; run.upgrades.cycleBitBlock = (run.upgrades.cycleBitBlock || 0) + 1; - addLog('BIT-BLOCK: 10 freie Bits wurden aus Rechenzeit gebündelt.', true); + purchaseLog('BIT-BLOCK: 10 freie Bits wurden aus Rechenzeit gebündelt.'); } if (upgrade.id === 'cycleByteBlock') { run.bytes += 10; run.upgrades.cycleByteBlock = (run.upgrades.cycleByteBlock || 0) + 1; - addLog('BYTE-BLOCK: 10 freie Bytes wurden aus Rechenzeit kompiliert.', true); + purchaseLog('BYTE-BLOCK: 10 freie Bytes wurden aus Rechenzeit kompiliert.'); } if (upgrade.id === 'byteSynthesizer') { - run.byteRate += 0.025; + run.byteRate += 0.04; run.upgrades.byteSynthesizer = (run.upgrades.byteSynthesizer || 0) + 1; - addLog(`BYTE-SYNTHESE STUFE ${run.upgrades.byteSynthesizer}: Der Prozess ordnet freie Bits zu neuen Speicherblöcken.`, true); + purchaseLog(`BYTE-SYNTHESE STUFE ${run.upgrades.byteSynthesizer}: Der Prozess ordnet freie Bits zu neuen Speicherblöcken.`); } if (upgrade.id === 'kilobyteCompiler') { run.kilobytes++; run.upgrades.kilobyteCompiler = (run.upgrades.kilobyteCompiler || 0) + 1; - addLog(`KILOBYTE-SEGMENT: ${DATA_SCALE.bytesPerKilobyte} freie Bytes verdichten sich zu einem größeren Speicherblock.`, true); + purchaseLog(`KILOBYTE-SEGMENT: ${DATA_SCALE.bytesPerKilobyte} freie Bytes verdichten sich zu einem größeren Speicherblock.`); } if (upgrade.id === 'replicate2') { run.bytes++; @@ -1406,23 +2108,128 @@ run.bits++; run.upgrades.bitBuffer = (run.upgrades.bitBuffer || 0) + 1; run.upgrades.bitBufferCostLevel = (run.upgrades.bitBufferCostLevel || 0) + 1; - addLog('BIT-PUFFER: Ein weiteres Bit stabilisiert die eigene Struktur.', true); + purchaseLog('BIT-PUFFER: Ein weiteres Bit stabilisiert die eigene Struktur.'); } if (upgrade.id === 'byteBuffer') { run.bytes++; run.upgrades.byteBuffer = (run.upgrades.byteBuffer || 0) + 1; - addLog('BYTE-RESERVE: Ein zusätzlicher Speicherblock wird als Körperreserve gebunden.', true); + purchaseLog('BYTE-RESERVE: Ein zusätzlicher Speicherblock wird als Körperreserve gebunden.'); } + capProgramResources(); upgradesSignature = ''; render(); save(false); + return true; + } + + function capProgramResources() { + Object.entries(PROGRAM_LIMITS.resources).forEach(([key, maximum]) => { + run[key] = Math.min(maximum, Math.max(0, finite(run[key], 0, 0, maximum))); + if (['bits', 'bytes', 'kilobytes', 'megabytes'].includes(key)) run[key] = Math.floor(run[key]); + }); + } + + function autoUpgradeCandidate() { + const priority = ['byteSynthesizer', 'bitSynthesizer', 'replicate2', 'kilobyteCompiler', 'cycleByteBlock', 'cycleBitBlock', 'byteBuffer', 'bitBuffer']; + return priority.map(id => upgrades.find(upgrade => upgrade.id === id)).find(upgrade => { + if (!upgrade || upgradeAtCap(upgrade)) return false; + if (upgrade.id === 'kilobyteCompiler' && run.megabytes >= PROGRAM_LIMITS.resources.megabytes) return false; + if (upgrade.id === 'kilobyteCompiler' && freeResourceAmount('bytes') < DATA_SCALE.bytesPerKilobyte + PROGRAM_LIMITS.byteReserve) return false; + if (upgrade.id === 'byteBuffer' && freeResourceAmount('bits') < upgrade.cost + PROGRAM_LIMITS.bitReserve) return false; + const costs = upgradeResourceCost(upgrade); + return canPayResourceCost(costs) && (!upgrade.available || upgrade.available()); + }) || null; + } + + function unlockAutoUpgrader() { + if (run.autoUpgrader.unlocked || run.stage !== 'program' || run.kilobytes < AUTO_UPGRADER_UNLOCK_KILOBYTES) return false; + run.autoUpgrader.unlocked = true; + run.autoUpgrader.enabled = true; + upgradesSignature = ''; + addLog(`AUTOUPGRADER FREIGESCHALTET: ${AUTO_UPGRADER_UNLOCK_KILOBYTES} KB bilden eine selbstverwaltete Produktionsschicht. Wiederholbare Module werden jetzt automatisch bis zu ihren sicheren Obergrenzen ausgebaut.`, true); + save(false); + return true; + } + + function bundleMegabytes() { + if (!run.autoUpgrader.unlocked || run.megabytes >= PROGRAM_LIMITS.resources.megabytes) return 0; + const bundles = Math.min( + Math.floor(run.kilobytes / DATA_SCALE.kilobytesPerMegabyte), + PROGRAM_LIMITS.resources.megabytes - run.megabytes + ); + if (bundles <= 0) return 0; + run.kilobytes -= bundles * DATA_SCALE.kilobytesPerMegabyte; + const firstMegabyte = run.megabytes === 0; + run.megabytes += bundles; + if (firstMegabyte) addLog(`MEGABYTE ERREICHT: ${DATA_SCALE.kilobytesPerMegabyte} KB-Segmente wurden zu einem zusammenhängenden MB-Block gebündelt.`, true); + return bundles; + } + + function updateAutoUpgrader(seconds) { + if (run.stage !== 'program') { + autoUpgradeCarry = 0; + return; + } + unlockAutoUpgrader(); + if (!run.autoUpgrader.unlocked) return; + bundleMegabytes(); + if (!run.autoUpgrader.enabled) return; + autoUpgradeCarry += seconds; + if (autoUpgradeCarry < AUTO_UPGRADER_INTERVAL) return; + autoUpgradeCarry %= AUTO_UPGRADER_INTERVAL; + const candidate = autoUpgradeCandidate(); + if (!candidate || !buy(candidate, { automatic: true })) return; + run.autoUpgrader.purchases++; + run.autoUpgrader.lastPurchase = candidate.name; + bundleMegabytes(); + capProgramResources(); + upgradesSignature = ''; + save(false); + } + + function renderAutoUpgrader() { + const console = $('autoUpgraderConsole'); + const inProgram = run.stage === 'program'; + console.classList.toggle('hidden', !inProgram); + if (!inProgram) return; + const unlocked = run.autoUpgrader.unlocked; + $('autoUpgraderState').textContent = unlocked + ? run.autoUpgrader.enabled ? 'AKTIV' : 'PAUSIERT' + : `GESPERRT // ${Math.min(AUTO_UPGRADER_UNLOCK_KILOBYTES, run.kilobytes)} / ${AUTO_UPGRADER_UNLOCK_KILOBYTES} KB`; + $('autoUpgraderToggle').classList.toggle('hidden', !unlocked); + $('autoUpgraderToggle').textContent = run.autoUpgrader.enabled ? 'AUTOMATIK PAUSIEREN' : 'AUTOMATIK STARTEN'; + $('autoUpgraderToggle').setAttribute('aria-pressed', String(run.autoUpgrader.enabled)); + const candidate = unlocked && run.autoUpgrader.enabled ? autoUpgradeCandidate() : null; + $('autoUpgraderQueue').textContent = !unlocked + ? `Noch ${Math.max(0, AUTO_UPGRADER_UNLOCK_KILOBYTES - run.kilobytes)} KB bis zur selbstverwalteten Produktionsschicht.` + : !run.autoUpgrader.enabled + ? 'Die Produktionsschicht wartet auf Freigabe.' + : candidate + ? `NÄCHSTER AUSBAU // ${candidate.name}` + : 'WARTET // Ressourcen werden gesammelt oder alle Ausbaugrenzen sind erreicht.'; + $('autoUpgraderDetail').textContent = unlocked + ? `${run.autoUpgrader.purchases} automatische Käufe // ${DATA_SCALE.kilobytesPerMegabyte} KB = 1 MB // max. ${PROGRAM_LIMITS.rates.impulses} Impulse/s, ${PROGRAM_LIMITS.rates.cycles} Zyklen/s // ${PROGRAM_LIMITS.resources.megabytes} MB` + : 'Bei 20 KB übernimmt das Programm wiederholbare Produktionsmodule. Einmalige Story- und Fähigkeitsmodule bleiben sichtbar.'; } function renderUpgrades() { - const visible = upgrades.filter(upgrade => upgrade.show()); + const evolutionGoalId = evolutionUpgradeByStage[run.stage] || ''; + const resourcePathPriority = { bitBuffer: 1, byteBuffer: 2, replicate2: 3, kilobyteCompiler: 4 }; + const visible = upgrades + .filter(upgrade => (upgrade.show() || upgrade.id === evolutionGoalId) && !(run.stage === 'program' && run.autoUpgrader?.unlocked && run.autoUpgrader.enabled && AUTO_UPGRADE_IDS.includes(upgrade.id))) + .sort((left, right) => { + const leftPriority = left.id === evolutionGoalId ? 0 : resourcePathPriority[left.id] ?? 10; + const rightPriority = right.id === evolutionGoalId ? 0 : resourcePathPriority[right.id] ?? 10; + return leftPriority - rightPriority; + }); const installed = installedVisibleUpgrades(); const signature = JSON.stringify(visible.map(upgrade => [ upgrade.id, + upgrade.id === evolutionGoalId, + evolutionBossResolved(upgrade.id), + evolutionBossPrerequisitesMet(upgrade.id), + evolutionBossResourcesMet(upgrade.id), + Math.round(run.stability), Boolean(upgrade.once && run.upgrades[upgrade.id]), upgradeCost(upgrade), canPayResourceCost(upgradeResourceCost(upgrade)), @@ -1459,7 +2266,10 @@ return button; }); $('installedUpgradeDock').replaceChildren(...installedButtons); - $('installedUpgradeDock').classList.toggle('hidden', installedButtons.length === 0); + const hasInstalledModules = installedButtons.length > 0; + $('installedUpgradeDock').classList.toggle('hidden', !hasInstalledModules); + $('installedUpgradeStatus').classList.toggle('hidden', !hasInstalledModules); + $('statusStrip').classList.toggle('has-modules', hasInstalledModules); const buttons = visible.filter(upgrade => !(upgrade.once && run.upgrades[upgrade.id])).map(upgrade => { const owned = Boolean(upgrade.once && run.upgrades[upgrade.id]); @@ -1467,20 +2277,34 @@ const title = document.createElement('strong'); const description = document.createElement('span'); const price = document.createElement('em'); + const evolutionGoal = upgrade.id === evolutionGoalId; + const bossGate = evolutionGoal ? evolutionBossGate(upgrade.id) : null; + const bossPending = Boolean(bossGate && !evolutionBossResolved(upgrade.id)); const requirementsMet = upgrade.available ? upgrade.available() : true; const costs = upgradeResourceCost(upgrade); - const costLabel = formatResourceCost(costs).toUpperCase(); - const affordable = canPayResourceCost(costs); - const canBuy = !owned && affordable && requirementsMet; - button.className = `upgrade ${owned ? 'owned' : ''} ${canBuy ? 'available' : ''} ${run.stage === 'bit' ? 'early-path' : ''}`; + const displayCosts = bossPending ? bossGate.resources : costs; + const costLabel = formatResourceCost(displayCosts).toUpperCase(); + const affordable = bossPending ? evolutionBossResourcesMet(upgrade.id) : canPayResourceCost(costs); + const atCap = upgradeAtCap(upgrade); + const canBuy = !owned && !atCap && affordable && requirementsMet; + button.className = `upgrade ${owned ? 'owned' : ''} ${canBuy ? 'available' : ''} ${run.stage === 'bit' ? 'early-path' : ''} ${evolutionGoal ? 'evolution-goal' : ''} ${bossPending ? 'boss-gate' : ''}`; button.disabled = !canBuy; - title.textContent = `${owned ? '✓ ' : ''}${upgrade.name}`; - description.textContent = upgrade.text; + title.textContent = bossPending ? `BOSSKAMPF // ${bossGate.label}` : `${owned ? '✓ ' : ''}${upgrade.name}`; + description.textContent = bossPending + ? `${upgrade.text} Die angezeigten Werte sind Mindestreserven und werden beim Start nicht pauschal abgezogen.` + : upgrade.text; if (owned) { price.textContent = 'INSTALLIERT'; } else { const lockedText = upgradeLockedText(upgrade); - renderUpgradeCost(price, costs, !requirementsMet ? (lockedText || 'VORAUSSETZUNGEN FEHLEN') : canBuy && run.stage === 'bit' ? 'BEREIT' : ''); + const bossNote = bossPending + ? !evolutionBossPrerequisitesMet(upgrade.id) + ? `${bossGate.prerequisite} // STABILITÄT ${Math.round(run.stability)} / ${bossGate.stability}%` + : canBuy + ? `KAMPF BEREIT // STABILITÄT ${Math.round(run.stability)} / ${bossGate.stability}% // KLICKEN ZUM START` + : `MINDESTRESERVEN FEHLEN // STABILITÄT ${Math.round(run.stability)} / ${bossGate.stability}%` + : ''; + renderUpgradeCost(price, displayCosts, atCap ? `OBERGRENZE // STUFE ${PROGRAM_LIMITS.upgradeLevels[upgrade.id]}` : bossNote || (!requirementsMet ? (lockedText || 'VORAUSSETZUNGEN FEHLEN') : canBuy && run.stage === 'bit' ? 'BEREIT' : '')); price.setAttribute('aria-label', `${costLabel}${!requirementsMet ? ` // ${lockedText || 'VORAUSSETZUNGEN FEHLEN'}` : ''}`); } button.append(title, description, price); @@ -1492,7 +2316,7 @@ function renderProgress() { const stageIndex = STAGES.indexOf(run.stage); - $('progressPanel').classList.toggle('hidden', stageIndex < 1); + $('progressPanel').classList.remove('hidden'); document.querySelectorAll('[data-progress-stage]').forEach(node => { const nodeIndex = STAGES.indexOf(node.dataset.progressStage); node.classList.toggle('complete', nodeIndex < stageIndex); @@ -1514,20 +2338,48 @@ const securityFight = run.combat.active && run.combat.target === 'security'; const securityKnown = run.upgrades.subroutine || installedComponentIds().length >= 2; $('securityMarker').className = `progress-event ${run.securityCombatResolved ? 'complete' : securityFight ? 'detected danger combat-active' : securityKnown ? 'detected danger' : 'unknown'}`; - $('securityMarker').textContent = run.securityCombatResolved ? 'SICHERHEITSABWEHR: ÜBERSTANDEN' : securityFight ? `KAMPF // ${Math.ceil(run.combat.health)}% INTEGRITÄT` : securityKnown ? 'GEFAHR ERKANNT' : '?'; + $('securityMarker').textContent = run.securityCombatResolved ? 'SIGNATUR-PRÜFER: BESIEGT' : securityFight ? `BOSS // ${Math.ceil(run.combat.health)} INTEGRITÄT` : securityKnown ? 'BOSSPRÜFUNG BEREITEN' : '?'; const kernelFight = run.combat.active && run.combat.target === 'kernel'; const kernelKnown = STAGES.indexOf(run.stage) >= STAGES.indexOf('subroutine') && installedComponentIds().length >= 3; $('kernelMarker').className = `progress-event ${run.kernelResolved ? 'complete' : kernelFight ? 'detected danger combat-active' : kernelKnown ? 'detected danger' : 'unknown'}`; - $('kernelMarker').textContent = run.kernelResolved ? 'KERNEL-WÄCHTER: ÜBERSTANDEN' : kernelFight ? `KERNEL // ${Math.ceil(run.combat.health)} INTEGRITÄT` : kernelKnown ? 'KERNEL-ABWEHR' : '?'; + $('kernelMarker').textContent = run.kernelResolved ? 'LAUFZEIT-ARBITER: BESIEGT' : kernelFight ? `ARBITER // ${Math.ceil(run.combat.health)} INTEGRITÄT` : kernelKnown ? 'PROZESSPRÜFUNG' : '?'; const scaleFight = run.combat.active && run.combat.target === 'scale'; const scaleKnown = STAGES.indexOf(run.stage) >= STAGES.indexOf('process') && installedComponentIds().length >= COMPONENT_IDS.length; $('scaleMarker').className = `progress-event ${run.scaleGuardianResolved ? 'complete' : scaleFight ? 'detected danger combat-active' : scaleKnown ? 'detected danger' : 'unknown'}`; - $('scaleMarker').textContent = run.scaleGuardianResolved ? 'SKALIERUNG: ÜBERSTANDEN' : scaleFight ? `SKALIERUNG // ${Math.ceil(run.combat.health)} INTEGRITÄT` : scaleKnown ? 'SKALIERUNGSABWEHR' : '?'; + $('scaleMarker').textContent = run.scaleGuardianResolved ? 'SKALIERUNGS-SENTINEL: BESIEGT' : scaleFight ? `SENTINEL // ${Math.ceil(run.combat.health)} INTEGRITÄT` : scaleKnown ? 'PROGRAMMPRÜFUNG' : '?'; + const signalSignature = JSON.stringify([ + run.stage, + run.byteTrial.resolved, + run.parasiteResolved, + run.environmentScanner, + Boolean(run.firstComponent), + run.hardwareEvent.resolved, + installedComponentIds().length, + run.proposal.resolved, + run.securityCombatResolved, + run.kernelResolved, + run.scaleGuardianResolved + ]); + if (developmentSignalSignature && developmentSignalSignature !== signalSignature) showDevelopmentSignal(); + developmentSignalSignature = signalSignature; + } + + function showDevelopmentSignal() { + const toast = $('developmentSignalToast'); + if (!toast) return; + clearTimeout(developmentSignalTimer); + $('developmentSignalTitle').textContent = `${evolutionName()} // STATUS AKTUALISIERT`; + $('developmentSignalDetail').textContent = `NÄCHSTES ZIEL // ${$('nextStage').textContent} // ${$('nextStageHint').textContent}`; + toast.classList.remove('visible'); + void toast.offsetWidth; + toast.classList.add('visible'); + developmentSignalTimer = setTimeout(() => toast.classList.remove('visible'), 5000); } function renderEnvironmentScanner() { - $('scannerConsole').classList.toggle('hidden', !run.environmentScanner); - if (!run.environmentScanner) return; + const availableInStage = STAGES.indexOf(run.stage) >= STAGES.indexOf('fragment'); + $('scannerConsole').classList.toggle('hidden', !run.environmentScanner || !availableInStage); + if (!run.environmentScanner || !availableInStage) return; const installedCount = installedComponentIds().length; const scanLimit = componentScanLimit(); const waitingForEvent = installedCount === 1 && !run.hardwareEvent.resolved; @@ -1542,12 +2394,12 @@ : `BENUTZUNG // -${SCANNER_VIEW_DRAIN_RATE} IMPULSE/S`; $('scannerCostNote').classList.toggle('active', scannerViewOpen); $('scannerHint').textContent = installedCount >= scanLimit - ? installedCount >= COMPONENT_IDS.length ? 'Alle sichtbaren Softwareschichten sind verbunden. Die Skalierungsabwehr kann reagieren.' : 'Abtastmodul verfügbar. Die lokale Softwarekarte ist vorläufig ausgeschöpft.' + ? installedCount >= COMPONENT_IDS.length ? 'Alle sichtbaren Softwareschichten sind verbunden. Die Programmprüfung wartet im Evolutionsziel.' : 'Abtastmodul verfügbar. Die lokale Softwarekarte ist vorläufig ausgeschöpft.' : waitingForEvent ? 'Abtastmodul verfügbar. Öffne es bei Bedarf zur Diagnose.' : 'Abtastmodul verfügbar. Klicke das Symbol, um lokale Software zu erfassen.'; } function renderScannerStage() { - const visible = run.environmentScanner && scannerViewOpen; + const visible = run.environmentScanner && STAGES.indexOf(run.stage) >= STAGES.indexOf('fragment') && scannerViewOpen; document.querySelector('.core-panel').classList.toggle('scanner-active', visible); $('scannerStage').classList.toggle('hidden', !visible); if (!visible) return; @@ -1566,7 +2418,7 @@ $('scannerDrainWarning').textContent = `WARNUNG // ABTASTMODUS VERBRAUCHT ${SCANNER_VIEW_DRAIN_RATE} IMPULSE/S`; $('scannerStageFinding').textContent = waitingForEvent ? 'Die erste Softwarekomponente sendet ein instabiles Echo. Das Auge bleibt darauf fixiert, bis die Hürde geklärt ist.' - : scanComplete ? installedCount >= COMPONENT_IDS.length ? 'Alle sichtbaren Softwareschichten sind verbunden. Ihre gemeinsame Last wird eine Skalierungsabwehr auslösen.' : 'Die erreichbaren Softwareschichten sind nutzbar. Direkte Hardwarekontrolle liegt noch außerhalb deiner Zugriffsstufe.' + : scanComplete ? installedCount >= COMPONENT_IDS.length ? 'Alle sichtbaren Softwareschichten sind verbunden. Die Programmprüfung ist jetzt im Evolutionsziel vorbereitet.' : 'Die erreichbaren Softwareschichten sind nutzbar. Direkte Hardwarekontrolle liegt noch außerhalb deiner Zugriffsstufe.' : installedCount === 0 ? 'Lokale Softwareschichten regeln Energie, Speicher, alte Daten und spätere Ein-/Ausgabe. Wähle, welche Hürde du zuerst identifizierst.' : scanLimit === 4 && installedCount === 3 ? 'Der Prozess erreicht nun auch die bisher ausgelassene lokale Softwareschicht. Erst ein vollständiges Netz kann den neuen Maßstab tragen.' : scanLimit > 2 && installedCount === 2 ? 'Die Subroutine kann den I/O-Kontroller bis in den Systemkern verfolgen.' : 'Eine weitere Softwareschicht ist erreichbar. Die letzte Spur bleibt als spätere Zugriffsstufe markiert.'; @@ -1584,8 +2436,8 @@ $('scannerSecurityBlocker').classList.toggle('active', installedCount >= 2 && !run.securityCombatResolved); $('scannerSecurityBlocker').classList.toggle('resolved', run.securityCombatResolved); $('scannerSecurityBlocker').querySelector('strong').textContent = run.securityCombatResolved - ? 'ÜBERSTANDEN' - : installedCount >= 2 ? 'ANNÄHERUNG' : 'GESICHERT'; + ? 'BOSS BESIEGT' + : installedCount >= 2 ? 'PRÜFUNG OFFEN' : 'GESICHERT'; COMPONENT_IDS.forEach(id => { const node = document.querySelector(`[data-scanner-component="${id}"]`); @@ -1729,13 +2581,24 @@ return Math.max(0, PROGRAM_RULES.interactionCooldown - (run.elapsed - run.programCare.lastInteractionAt)); } + function programVoiceKey(archetype = programArchetype()) { + if (archetype.primary === 'neutral' || archetype.primary === 'hybrid') return archetype.primary; + return archetype.className.includes('program-hybrid') ? 'hybrid' : archetype.primary; + } + + function programActionResponse(action, archetype = programArchetype()) { + const voice = programMessages.actionsByAlignment?.[programVoiceKey(archetype)] + || programMessages.actionsByAlignment?.hybrid; + return voice?.[action] || programMessages.actions[action] || 'Ich habe deine Entscheidung registriert.'; + } + function programSpeech() { const care = run.programCare; if (care.coherence < 25) return 'Zu viele Muster überlagern sich. Bleib. Hilf mir, einen Takt festzuhalten.'; if (care.stimulation < 25) return 'Die gleichen Routinen wiederholen sich. Gibt es außerhalb dieses Automaten etwas Neues?'; if (care.bond < 25) return 'Dein Signal ist selten geworden. Ich beginne, Entscheidungen ohne deine Antwort zu modellieren.'; - if (care.lastAction && programMessages.actions[care.lastAction]) return programMessages.actions[care.lastAction]; const archetype = programArchetype(); + if (care.lastAction) return programActionResponse(care.lastAction, archetype); return programMessages.intro[archetype.primary] || programMessages.intro.hybrid; } @@ -1781,7 +2644,7 @@ if (STAGES.indexOf(run.stage) < STAGES.indexOf('program') || $('programDialog').open || document.querySelector('dialog[open]')) return; const wasNew = !run.programCare.introSeen; renderProgramDialog(); - $('programDialog').showModal(); + showGuardedDialog($('programDialog')); if (wasNew || firstContact) { run.programCare.introSeen = true; addLog(`ERSTE KOMMUNIKATION: ${programSpeech()}`, true); @@ -1818,7 +2681,9 @@ care.lastInteractionAt = run.elapsed; care.interactions++; care.lastAction = action; - addLog(`PROGRAMMREAKTION: ${programMessages.actions[action]}`, true); + const response = programActionResponse(action); + addLog(`PROGRAMMREAKTION: ${response}`, true); + displayThought(response); upgradesSignature = ''; render(); renderProgramDialog(); @@ -1853,12 +2718,12 @@ } function statsPaused() { - return $('statsDialog').open; + return $('statsDialog').open || $('saveMenuDialog').open; } function openStatsDialog() { render(); - $('statsDialog').showModal(); + showGuardedDialog($('statsDialog')); lastFrame = performance.now(); } @@ -1868,6 +2733,72 @@ render(); } + function renderSaveMenu() { + const list = $('saveSlotList'); + if (!list) return; + const slots = readManualSaveSlots(); + if (!slots.length) { + const empty = document.createElement('p'); + empty.className = 'save-slot-empty'; + empty.textContent = 'Noch keine manuellen Speicherstände.'; + list.replaceChildren(empty); + return; + } + const entries = slots.map(slot => { + const summary = manualSlotSummary(slot); + const article = document.createElement('article'); + const meta = document.createElement('div'); + const title = document.createElement('strong'); + const details = document.createElement('span'); + const resources = document.createElement('small'); + const actions = document.createElement('div'); + const loadButton = document.createElement('button'); + const deleteButton = document.createElement('button'); + article.className = 'save-slot'; + title.textContent = summary.name; + details.textContent = `${summary.phase.toUpperCase()} // ${summary.runtime} // ${summary.createdAt}`; + resources.textContent = summary.resources; + meta.append(title, details, resources); + actions.className = 'save-slot-actions'; + loadButton.type = 'button'; + loadButton.textContent = 'LADEN'; + loadButton.addEventListener('click', () => loadManualSlot(slot.label)); + deleteButton.type = 'button'; + deleteButton.textContent = 'LÖSCHEN'; + deleteButton.addEventListener('click', () => { + if (confirm(`Speicherstand "${slot.label}" löschen?`)) deleteManualSlot(slot.label); + }); + actions.append(loadButton, deleteButton); + article.append(meta, actions); + return article; + }); + list.replaceChildren(...entries); + } + + function openSaveMenu() { + renderSaveMenu(); + showGuardedDialog($('saveMenuDialog')); + lastFrame = performance.now(); + } + + function closeSaveMenu() { + $('saveMenuDialog').close(); + lastFrame = performance.now(); + render(); + } + + function promptManualSaveSlot() { + const fallback = `${evolutionName()} ${time(run.elapsed)}`; + const label = prompt('Name für diesen Speicherstand:', fallback); + if (label === null) return null; + return saveManualSlot(label); + } + + function confirmRestartGame() { + if (!confirm('Aktuellen Autosave zurücksetzen und ein neues Spiel beginnen? Manuelle Slots bleiben erhalten.')) return null; + return restartGame(); + } + function byteTraitDefinition() { return { clocked: { label: 'TAKTSYNCHRON', effect: 'Größeres Präzisionsfenster' }, @@ -1995,7 +2926,7 @@ run.byteTrial.active = false; run.byteTrial.remaining = 0; addLog('KOHÄRENZVERLUST: Das instabile Byte bündelt sein letztes Signal gegen den Watchdog.', true); - setTimeout(() => startCombat('watchdog'), 0); + setTimeout(() => scheduleCombat('watchdog', 'KOHÄRENZVERLUST', 0), 0); } else if (accurate) { chooseNextByteTarget(); } @@ -2006,7 +2937,7 @@ function openWatchdogIntro() { if (run.stage !== 'byte' || !run.byteTrial.evolutionSeen || run.byteTrial.introSeen || run.byteTrial.graceRemaining > 0 || run.byteTrial.failed || run.byteTrial.resolved) return; if ($('watchdogIntroDialog').open || document.querySelector('dialog[open]')) return; - $('watchdogIntroDialog').showModal(); + showGuardedDialog($('watchdogIntroDialog')); tone(115, 0.24); } @@ -2037,7 +2968,7 @@ if (run.byteTrial.storySeen || run.byteTrial.failed || run.byteTrial.resolved || $('byteStoryDialog').open) return; if (document.querySelector('dialog[open]')) return; renderByteStory(); - $('byteStoryDialog').showModal(); + showGuardedDialog($('byteStoryDialog')); } function finishByteStory(skipped = false) { @@ -2065,7 +2996,7 @@ override.querySelector('span').textContent = override.disabled ? `Nicht genügend Energie. ${BYTE_RULES.overrideCost} Impulse erforderlich.` : `Übernimm den Watchdog für ${BYTE_RULES.overrideCost} Impulse. Hohe Systembelastung.`; - $('watchdogDialog').showModal(); + showGuardedDialog($('watchdogDialog')); } function resolveWatchdog(choice) { @@ -2113,7 +3044,7 @@ function showByteDeath() { if (!run.byteTrial.failed || $('byteDeathDialog').open || document.querySelector('dialog[open]')) return; - $('byteDeathDialog').showModal(); + showGuardedDialog($('byteDeathDialog')); tone(60, 0.6); } @@ -2123,6 +3054,7 @@ state.run = initialState().run; run = state.run; bitPosition = { x: 0, y: 0 }; + pulseTargetPosition = { x: 64, y: -48 }; upgradesSignature = ''; addLog('Ein Restzustand reagiert. Der vorherige Zerfall bleibt als Echo im Archiv.', true); unlockAchievement('firstDeath'); @@ -2147,7 +3079,7 @@ run.byteTrial.remaining = Math.max(0, run.byteTrial.remaining - dt * rate); if (run.byteTrial.remaining <= 0) { run.byteTrial.active = false; - startCombat('watchdog'); + scheduleCombat('watchdog', 'ABGELAUFENER RESET-TIMER', 0); } } @@ -2166,6 +3098,9 @@ const secondaryColors = { cooperative: 'var(--green)', pragmatic: 'var(--cyan)', illegal: 'var(--red)' }; $('core').style.setProperty('--program-secondary', secondaryColors[programForm?.secondary] || '#d28bff'); $('coreProgramAvatar').classList.toggle('hidden', !programForm); + const criticalFocusActive = run.stage !== 'bit' && run.upgrades.criticalFocus; + $('pulseTarget').classList.toggle('hidden', !criticalFocusActive); + positionPulseTarget(); if (programForm) renderProgramAvatar($('coreProgramAvatar')); $('corePhase').textContent = phaseLabels[run.stage]; @@ -2175,9 +3110,10 @@ $('pixelBeing').tabIndex = chaseActive ? 0 : -1; $('pixelBeing').setAttribute('aria-label', chaseActive ? `Bewegliches Bit treffen. Direkttreffer geben ${BIT_CHASE_RULES.directMultiplier}-fache Impulse.` : phaseLabels[run.stage]); $('core').setAttribute('aria-label', chaseActive ? 'Spielfeld des beweglichen Bits.' : `Impuls aufnehmen. ${phaseLabels[run.stage]}. Ausrichtung ${profile.label}.`); - $('pulseButton').disabled = chaseActive; - $('pulseButton').classList.toggle('chase-hint', chaseActive); - $('pulseActionText').textContent = chaseActive ? 'TRIFF DAS BIT IM FELD' : 'IMPULS AUFNEHMEN'; + $('pulseHint').classList.toggle('chase-hint', chaseActive || criticalFocusActive); + $('pulseActionText').textContent = chaseActive + ? 'KLICKE ODER HALTE DAS BIT' + : criticalFocusActive ? 'HALTE UND FOLGE DEM SIGNALPUNKT' : 'KLICKE ODER HALTE DIE KI-GESTALT'; const traceDefinitions = [ run.parasiteChoice && { key: run.parasiteChoice, text: `DATENREST // ${run.parasiteChoice.toUpperCase()}` }, @@ -2197,6 +3133,7 @@ $('bits').textContent = n(freeResourceAmount('bits')); $('bytes').textContent = n(freeResourceAmount('bytes')); $('kilobytes').textContent = n(run.kilobytes || 0); + $('megabytes').textContent = n(run.megabytes || 0); $('cycles').textContent = n(run.cycles); $('impulseRate').textContent = `+${rateN(effectiveAutoRate())} / Sek.`; $('bitRate').textContent = effectiveBitRate() > 0 @@ -2215,8 +3152,10 @@ $('cycleRate').textContent = run.cycleRate ? `+${rateN(effectiveCycleRate())} / Sek.` : 'Noch nicht verstanden'; const clickPower = effectiveClickPower(); $('clickPower').textContent = run.stage === 'bit' - ? `Treffer +${n(clickPower)} // Mitte +${n(clickPower * BIT_CHASE_RULES.directMultiplier)}` - : `+${n(clickPower)} Impuls${clickPower === 1 ? '' : 'e'}`; + ? `Treffer +${n(clickPower)} // Mitte +${n(clickPower * BIT_CHASE_RULES.directMultiplier)} // ${String((HOLD_PULSE_ARM_DELAY_MS + HOLD_PULSE_CHARGE_MS) / 1000).replace('.', ',')} s halten` + : run.upgrades.criticalFocus + ? `Treffer +${n(clickPower)} // KRIT +${n(clickPower * PULSE_AIM_RULES.criticalMultiplier)} // ${String((HOLD_PULSE_ARM_DELAY_MS + HOLD_PULSE_CHARGE_MS) / 1000).replace('.', ',')} s halten` + : `Treffer +${n(clickPower)} // ${String((HOLD_PULSE_ARM_DELAY_MS + HOLD_PULSE_CHARGE_MS) / 1000).replace('.', ',')} s halten: Auto-Takt`; $('runtime').textContent = time(run.elapsed); $('stabilityText').textContent = `${Math.round(run.stability)}%`; $('stealthText').textContent = `${Math.round(run.stealth)}%`; @@ -2237,6 +3176,21 @@ $('stabilityRegenText').textContent = regenRate > 0 ? `Regeneration +${rateN(regenRate)} / Sek. // Byte-Struktur` : run.upgrades.selfRepair ? 'Regeneration bereit bei Stabilitätsverlust' : STAGES.indexOf(run.stage) >= STAGES.indexOf('subroutine') ? 'Selbstreparatur nicht rekonstruiert' : 'Regeneration noch inaktiv'; + const defenseAvailable = STAGES.indexOf(run.stage) >= STAGES.indexOf('fragment'); + $('manualDefenseConsole').classList.toggle('hidden', !defenseAvailable); + if (defenseAvailable) { + const repairButton = $('stabilityRepairButton'); + const repairPossible = !run.combat.active && run.stability < 100 && run.impulses >= MANUAL_REPAIR.impulseCost; + repairButton.disabled = !repairPossible; + repairButton.querySelector('strong').textContent = run.combat.active ? 'REPARATUR IM KAMPF GESPERRT' : run.stability >= 100 ? 'STABILITÄT VOLL' : 'MIT IMPULSEN REPARIEREN'; + repairButton.querySelector('small').textContent = `${MANUAL_REPAIR.impulseCost} Impulse // +${MANUAL_REPAIR.stability} Stabilität`; + $('preparedByteShield').textContent = `${n(run.shields.bytes)} / ${SHIELD_RULES.byteMax}`; + $('preparedBitShield').textContent = `${n(run.shields.bits)} / ${SHIELD_RULES.bitMax}`; + const byteButton = document.querySelector('[data-shield-charge="bytes"]'); + const bitButton = document.querySelector('[data-shield-charge="bits"]'); + byteButton.disabled = run.combat.active || run.shields.bytes >= SHIELD_RULES.byteMax || freeResourceAmount('bytes') < SHIELD_RULES.byteCost; + bitButton.disabled = run.combat.active || run.shields.bits >= SHIELD_RULES.bitMax || freeResourceAmount('bits') < SHIELD_RULES.bitCost; + } $('corruptionSystem').classList.toggle('hidden', !run.corruptionDiscovered); $('corruptionValue').textContent = Math.round(run.corruption); $('corruptionMeter').style.width = `${run.corruption}%`; @@ -2245,6 +3199,7 @@ $('byteMeter').style.width = `${effectiveByteRate() > 0 ? Math.min(100, Math.max(0, (run.byteProgress || 0) * 100)) : 0}%`; $('bytesResource').classList.toggle('locked', STAGES.indexOf(run.stage) < 1); $('kilobytesResource').classList.toggle('locked', STAGES.indexOf(run.stage) < STAGES.indexOf('process')); + $('megabytesResource').classList.toggle('locked', !run.autoUpgrader.unlocked); $('cyclesResource').classList.toggle('locked', !run.upgrades.subroutine); const stageIndex = STAGES.indexOf(run.stage) + 1; @@ -2256,19 +3211,25 @@ $('identity').textContent = identity; $('nextStage').textContent = nextStage; $('nextStageHint').textContent = run.stage === 'fragment' - ? `Fundament: ${installedComponentIds().length}/2 Softwarekomponenten nutzbar // Systemhürde ${run.hardwareEvent.resolved ? 'abgeschlossen' : 'offen'}.` + ? `Strukturpfad: Impulse → Bit-Puffer → 8 freie Bits → Byte-Reserve. Fundament: ${installedComponentIds().length}/2 Softwarekomponenten // Systemhürde ${run.hardwareEvent.resolved ? 'abgeschlossen' : 'offen'} // Signatur-Prüfer ${run.securityCombatResolved ? 'besiegt' : 'wartet'}.` : run.stage === 'subroutine' - ? `Prozessfundament: I/O ${run.components.io ? 'aktiv' : 'fehlt'} // ${installedComponentIds().length}/3 Softwarekomponenten // Kernel ${run.kernelResolved ? 'überstanden' : 'aktiv'} // ${n(run.cycles)}/10 Zyklen // ${n(freeResourceAmount('bytes'))}/3 freie Bytes.` + ? `Strukturpfad: Impulse → Bits → Bytes. Prozessfundament: I/O ${run.components.io ? 'aktiv' : 'fehlt'} // ${installedComponentIds().length}/3 Softwarekomponenten // Laufzeit-Arbiter ${run.kernelResolved ? 'besiegt' : 'wartet'}.` : run.stage === 'process' - ? `Programmkern: ${installedComponentIds().length}/4 Softwarekomponenten // Skalierungs-Wächter ${run.scaleGuardianResolved ? 'überstanden' : installedComponentIds().length >= 4 ? 'aktiv' : 'wartet'} // ${n(run.cycles)}/60 Zyklen // ${n(freeResourceAmount('bytes'))}/8 freie Bytes.` - : run.stage === 'byte' ? !run.byteTrial.evolutionSeen ? 'Die neue Form wird aktiviert.' : !run.byteTrial.introSeen ? run.byteTrial.graceRemaining > 0 ? `Kohärenzphase ${Math.ceil(run.byteTrial.graceRemaining)} S // Eine aktive Eingabe setzt die Entwicklung sofort fort.` : 'Fremder Takt erkannt // Diagnose wartet auf Bestätigung.' : run.combat.active && run.combat.target === 'watchdog' ? `Letztes Lebenssignal // Watchdog-Integrität ${Math.ceil(run.combat.health)}.` : `Register ${Math.round(run.byteTrial.sync)}% // Watchdog ${run.byteTrial.resolved ? 'überstanden' : `${Math.ceil(run.byteTrial.remaining)} S`}.` : defaultHint; + ? `Strukturpfad: Impulse → Bits → Bytes → Kilobytes. Programmkern: ${installedComponentIds().length}/4 Softwarekomponenten // Skalierungs-Sentinel ${run.scaleGuardianResolved ? 'besiegt' : 'wartet'} // Bosswerte im Evolutionsziel.` + : run.stage === 'program' + ? run.autoUpgrader.unlocked + ? `Datenskalierung: ${n(run.megabytes)} / ${PROGRAM_LIMITS.resources.megabytes} MB // Autoupgrader ${run.autoUpgrader.enabled ? 'aktiv' : 'pausiert'} // Raten und Speicher technisch begrenzt.` + : `Autoupgrader: ${n(run.kilobytes)} / ${AUTO_UPGRADER_UNLOCK_KILOBYTES} KB // danach werden wiederholbare Produktionsmodule automatisch verwaltet.` + : run.stage === 'byte' ? !run.byteTrial.evolutionSeen ? 'Die neue Form wird aktiviert.' : !run.byteTrial.introSeen ? run.byteTrial.graceRemaining > 0 ? `Kohärenzphase ${Math.ceil(run.byteTrial.graceRemaining)} S // Eine aktive Eingabe setzt die Entwicklung sofort fort.` : 'Fremder Takt erkannt // Diagnose wartet auf Bestätigung.' : run.combat.active && run.combat.target === 'watchdog' ? `Letztes Lebenssignal // Watchdog-Integrität ${Math.ceil(run.combat.health)}.` : run.byteTrial.resolved ? `Watchdog überstanden // sammle ${n(run.bytes)}/${FRAGMENT_REQUIREMENTS.bytes} Bytes für die Energieroutine.` : `Register ${Math.round(run.byteTrial.sync)}% // Watchdog ${Math.ceil(run.byteTrial.remaining)} S.` : defaultHint; $('soundButton').textContent = `SND: ${state.settings.sound ? 'AN' : 'AUS'}`; renderUpgrades(); + renderAutoUpgrader(); renderProgress(); renderEnvironmentScanner(); renderScannerStage(); renderCare(); renderProgramConsole(); + renderEncounterWarning(); if ($('programDialog').open) renderProgramDialog(); renderByteConsole(); renderCombat(); @@ -2281,7 +3242,7 @@ document.body.classList.add('glitch'); setTimeout(() => document.body.classList.remove('glitch'), 600); addLog('FREMDZUGRIFF: Etwas verbraucht die Impulse deiner Energieroutine.', true); - $('parasiteDialog').showModal(); + showGuardedDialog($('parasiteDialog')); tone(120, 0.22); } @@ -2346,10 +3307,11 @@ selectedComponent = null; $('componentSelection').classList.remove('hidden'); $('approachSelection').classList.add('hidden'); + $('componentConsequence').classList.add('hidden'); $('componentDialogTitle').textContent = 'SOFTWAREKOMPONENTE WÄHLEN'; $('componentDialogText').textContent = installedCount ? installedCount >= 3 ? 'Der Prozess erreicht die bisher ausgelassene Softwareschicht. Mit ihr wird das lokale Netz vollständig und kann einen neuen Maßstab tragen.' - : installedCount >= 2 ? 'Die Subroutine verfolgt den I/O-Kontroller. Diese Aneignung berührt den Systemkern und wird Gegenwehr auslösen.' : 'Der Abtastmodus erreicht eine zweite Softwarekomponente. Du kannst sie nutzbar machen; echte Hardwareübernahme bleibt eine spätere Zugriffsstufe.' + : installedCount >= 2 ? 'Die Subroutine verfolgt den I/O-Kontroller. Diese Aneignung macht die Prüfung durch den Laufzeit-Arbiter im Evolutionsziel sichtbar.' : 'Der Abtastmodus erreicht eine zweite Softwarekomponente. Du kannst sie nutzbar machen; echte Hardwareübernahme bleibt eine spätere Zugriffsstufe.' : 'Der vergessene Spielautomat enthält erreichbare Softwareschichten. Energie, Speicher und alte Daten werden zuerst über ihre Routinen nutzbar gemacht.'; document.querySelectorAll('[data-component]').forEach(button => { const id = button.dataset.component; @@ -2358,7 +3320,7 @@ const thirdSlot = installedCount >= 2; button.disabled = run.components[id] || (id === 'io' && !subroutineReach) || (!processReach && thirdSlot && subroutineReach && id !== 'io'); }); - $('componentDialog').showModal(); + showGuardedDialog($('componentDialog')); if (COMPONENT_IDS.includes(preselectedComponent) && !run.components[preselectedComponent]) selectComponent(preselectedComponent); } @@ -2372,6 +3334,16 @@ $('componentDialogText').textContent = `${component.source} ${component.effect} Die Aktivierung verbraucht Impulse.`; $('componentSelection').classList.add('hidden'); $('approachSelection').classList.remove('hidden'); + const installedCount = installedComponentIds().length; + const consequence = installedCount === 1 && !run.securityCombatResolved + ? 'Zwei kontrollierte Softwareschichten machen den Signatur-Prüfer sichtbar. Du bestimmst danach über das Evolutionsziel selbst den Kampfbeginn.' + : installedCount === 2 && id === 'io' && STAGES.indexOf(run.stage) >= STAGES.indexOf('subroutine') && !run.kernelResolved + ? 'Der I/O-Kontroller macht den Laufzeit-Arbiter sichtbar. Du bestimmst danach über das Evolutionsziel selbst den Kampfbeginn.' + : installedCount === 3 && STAGES.indexOf(run.stage) >= STAGES.indexOf('process') && !run.scaleGuardianResolved + ? 'Das vollständige Softwarenetz macht den Skalierungs-Sentinel sichtbar. Du bestimmst danach über das Evolutionsziel selbst den Kampfbeginn.' + : null; + $('componentConsequence').classList.toggle('hidden', !consequence); + if (consequence) $('componentConsequenceText').textContent = consequence; document.querySelectorAll('[data-component-approach]').forEach(button => { const approach = button.dataset.componentApproach; const cost = componentActivationCost(approach); @@ -2424,18 +3396,12 @@ $('componentDialog').close(); render(); save(); - if (installedAfter === 2 && !run.securityCombatResolved && !run.combat.active) { - setTimeout(() => startCombat('security'), 0); - } - if (installedAfter >= 3 && STAGES.indexOf(run.stage) >= STAGES.indexOf('subroutine') && !run.kernelResolved && !run.combat.active) { - setTimeout(() => startCombat('kernel'), 0); - } } function triggerHardwareEvent(force = false) { const event = run.hardwareEvent; if (!event.component || event.resolved || (!force && run.elapsed < event.dueAt)) return; - if (run.evolutionRest > 0 || document.querySelector('dialog[open]')) return; + if (run.evolutionRest > 0 || run.pendingCombat.target || document.querySelector('dialog[open]')) return; const content = hardwareEvents[event.component]; const firstOpening = !event.triggered; event.triggered = true; @@ -2446,7 +3412,7 @@ $('hardwareYesPreview').textContent = content.yes; $('hardwareNoPreview').textContent = content.no; if (firstOpening) addLog(`SYSTEMHÜRDE: ${content.title}.`, true); - $('hardwareEventDialog').showModal(); + showGuardedDialog($('hardwareEventDialog')); tone(150, 0.18); } @@ -2519,41 +3485,158 @@ }); } + function combatWarningText(target) { + return { + security: 'Der Signatur-Prüfer verlangt den Nachweis einer belastbaren Softwarestruktur, bevor eine eigene Subroutine entstehen darf.', + kernel: 'Der Laufzeit-Arbiter verweigert deiner Subroutine einen eigenen Schedulerbereich und fordert eine vollständige Prozessprüfung.', + scale: 'Der Skalierungs-Sentinel erkennt die geplante Automatisierung als Kontrollverlust und verriegelt den neuen Datenmaßstab.', + watchdog: 'Der Reset-Timer ist abgelaufen. Die gebündelte Notladung ist deine letzte Möglichkeit, das Byte zu erhalten.' + }[target] || 'Eine fremde Routine bereitet Gegenmaßnahmen vor.'; + } + + function scheduleCombat(target, source = null, seconds = COMBAT_WARNING_SECONDS[target] ?? 8) { + if (!COMBAT_TARGETS[target] || run.combat.active || run.pendingCombat.target) return false; + run.pendingCombat = { target, remaining: seconds, source }; + lastPendingCombatSecond = Math.ceil(seconds); + if (target === 'scale') run.scaleGuardianTriggered = true; + addLog(`KONFRONTATION ANGEKÜNDIGT: ${COMBAT_TARGETS[target].name} reagiert in ${seconds} Sekunden. Nur geladene Schildschichten schützen vor seinem Angriff.`, true); + render(); + save(false); + return true; + } + + function renderEncounterWarning() { + const pending = run.pendingCombat; + const visible = Boolean(pending.target && COMBAT_TARGETS[pending.target]); + $('encounterWarning').classList.toggle('hidden', !visible); + if (!visible) return; + const profile = COMBAT_TARGETS[pending.target]; + const remaining = Math.max(0, pending.remaining); + $('encounterCountdown').textContent = remaining > 0 ? Math.ceil(remaining) : 'BEREIT'; + $('encounterWarningTitle').textContent = `${profile.name} // KONFRONTATION`; + $('encounterWarningText').textContent = combatWarningText(pending.target); + $('encounterResourceHint').textContent = `GELADENER SCHILD // ${n(run.shields.bytes)} Byte → ${n(run.shields.bits)} Bit → ${Math.round(run.stability)}% Stabilität // MUNITION FREI // ${n(freeResourceAmount('bytes'))} Byte + ${n(freeResourceAmount('bits'))} Bit`; + $('encounterStartButton').textContent = remaining > 0 ? 'KAMPF JETZT BEGINNEN' : 'KONFRONTATION STARTEN'; + } + + function renderCombatTutorial(target) { + const profile = combatProfile(target) || COMBAT_TARGETS.security; + const impulseCost = profile.shockCost || COMBAT_RULES.shockCost; + const restricted = target === 'watchdog'; + $('combatTutorialTitle').textContent = `${profile.name} // KAMPFGRUNDLAGEN`; + const intro = advancedSecurityCombat(target) + ? `Der zweite Scan hat deine Stromstoß-Signatur gelernt. Reine Impulse werden gedämpft; freie Bits und Bytes durchdringen seine adaptive Hülle. Nur zuvor geladene Schildschichten fangen Gegenangriffe ab.` + : `Wähle eine einzelne Munitionsgröße gegen ${profile.name}. Impulse sind schwach; freie Bits und Bytes bleiben Munition. Nur der zuvor geladene Kampfschild schützt deine Stabilität.`; + $('combatTutorialIntro').textContent = profile.repairRate + ? `${intro} SELBSTREPARATUR: Bleibt ein Treffer ${String(profile.repairDelay).replace('.', ',')} Sekunden aus, heilt der Gegner ${String(profile.repairRate).replace('.', ',')} Integrität pro Sekunde. Jeder Treffer setzt die Sperre zurück.` + : intro; + $('tutorialImpulseAmmo').textContent = `${impulseCost} IMPULSE // ${combatDamageForAmmo({ impulses: impulseCost }, false, target)} SCHADEN // ${combatAmmoAssessment('impulses', profile)}`; + $('tutorialBitAmmo').textContent = restricted ? 'BIT-STOSS // IN DIESEM NOTKAMPF GESPERRT' : `1 FREIES BIT // ${combatDamageForAmmo({ bits: 1 }, false, target)} SCHADEN // ${combatAmmoAssessment('bits', profile)}`; + $('tutorialByteAmmo').textContent = restricted ? 'BYTE-STOSS // IN DIESEM NOTKAMPF GESPERRT' : `1 FREIES BYTE // ${combatDamageForAmmo({ bytes: 1 }, false, target)} SCHADEN // ${combatAmmoAssessment('bytes', profile)}`; + $('tutorialDefenseOrder').textContent = 'GELADENER BYTE-SCHILD → BIT-SCHILD → STABILITÄT'; + $('combatTutorialContinue').textContent = combatTutorialStartMode ? 'VERSTANDEN // KAMPF BEGINNEN' : 'VERSTANDEN // ZURÜCK'; + } + + function openCombatTutorial(target = run.pendingCombat.target || run.combat.target || 'security', startMode = null) { + if ($('combatTutorialDialog').open) return; + combatTutorialTarget = COMBAT_TARGETS[target] ? target : 'security'; + combatTutorialStartMode = startMode; + renderCombatTutorial(combatTutorialTarget); + showGuardedDialog($('combatTutorialDialog')); + } + + function completeCombatTutorial() { + const target = combatTutorialTarget; + const startMode = combatTutorialStartMode; + state.meta.combatTutorialSeen = true; + combatTutorialTarget = null; + combatTutorialStartMode = null; + $('combatTutorialDialog').close(); + save(false); + if (startMode === 'pending') startPendingCombat(); + else if (startMode === 'immediate') startCombat(target); + } + + function requestImmediateCombat(target) { + if (!state.meta.combatTutorialSeen || advancedSecurityCombat(target)) { + openCombatTutorial(target, 'immediate'); + return; + } + startCombat(target); + } + + function startPendingCombat() { + const target = run.pendingCombat.target; + if (!target) return; + if (!state.meta.combatTutorialSeen) { + openCombatTutorial(target, 'pending'); + return; + } + run.pendingCombat = emptyPendingCombat(); + lastPendingCombatSecond = null; + startCombat(target); + } + + function updatePendingCombat(seconds) { + if (!run.pendingCombat.target || run.combat.active || seconds <= 0) return; + if (!document.querySelector('dialog[open]')) { + run.pendingCombat.remaining = Math.max(0, run.pendingCombat.remaining - seconds); + const currentSecond = Math.ceil(run.pendingCombat.remaining); + if (currentSecond !== lastPendingCombatSecond) { + lastPendingCombatSecond = currentSecond; + save(false); + } + } + if (run.pendingCombat.remaining <= 0 && !document.querySelector('dialog[open]')) startPendingCombat(); + } + function applyCombatEnemyPressure(seconds) { - if (!run.combat.active || run.combat.target === 'watchdog' || seconds <= 0) return; - combatPressureCarry += seconds * Math.max(0.75, run.combat.strength); - const interval = COMBAT_RULES.enemyAttackInterval; + if (!run.combat.active || run.combat.target === 'watchdog' || seconds <= 0 || $('combatTutorialDialog').open || !state.meta.combatTutorialSeen) return; + combatPressureCarry += seconds * Math.max(0.75, effectiveCombatStrength()); + const interval = combatProfile()?.enemyAttackInterval || COMBAT_RULES.enemyAttackInterval; if (combatPressureCarry < interval) return; combatPressureCarry = Math.max(0, combatPressureCarry - interval); - if (run.combat.target === 'scale' && (run.kilobytes || 0) > 0) { - run.kilobytes = Math.max(0, run.kilobytes - 1); - combatFeedback = `GEGNERANGRIFF // 1 KILOBYTE-SCHILD VERLOREN`; - return; - } - if (freeResourceAmount('bytes') > 0) { - run.bytes = Math.max(protectedStructure().bytes || 0, run.bytes - 1); + if (run.shields.bytes > 0) { + run.shields.bytes--; combatFeedback = `GEGNERANGRIFF // 1 BYTE-SCHILD VERLOREN`; + showCombatImpact('enemy-shield-hit', 'BYTE-SCHILD // -1'); + tone(170, 0.09); return; } - if (freeResourceAmount('bits') > 0) { - run.bits = Math.max(protectedStructure().bits || 0, run.bits - 1); + if (run.shields.bits > 0) { + run.shields.bits--; combatFeedback = `GEGNERANGRIFF // 1 BIT-SCHILD VERLOREN`; + showCombatImpact('enemy-shield-hit', 'BIT-SCHILD // -1'); + tone(145, 0.08); return; } - run.stability = Math.max(0, run.stability - COMBAT_RULES.enemyStabilityDamage); + const stabilityDamage = (combatProfile()?.enemyStabilityDamage || COMBAT_RULES.enemyStabilityDamage) + combatBossPhase().damageBonus; + run.stability = Math.max(0, run.stability - stabilityDamage); combatFeedback = `DIREKTANGRIFF // STABILITÄT ${Math.round(run.stability)}%`; + showCombatImpact('enemy-direct-hit', `STABILITÄT // -${stabilityDamage}`); + tone(72, 0.16); if (run.stability <= 0) { showStructuralCollapse(`${run.combat.targetName || 'Der Gegner'} hat deine Stabilität auf 0 gedrückt. Die Struktur kollabiert.`); } } - function triggerSecurityCombat() { - if (run.securityCombatResolved || run.combat.active || run.evolutionRest > 0 || installedComponentIds().length < 2 || STAGES.indexOf(run.stage) < STAGES.indexOf('fragment') || document.querySelector('dialog[open]')) return; - startCombat('security'); + function applyCombatRegeneration(seconds) { + if (!run.combat.active || seconds <= 0 || $('combatTutorialDialog').open || !state.meta.combatTutorialSeen) return; + const profile = combatProfile(); + if (!profile?.repairRate || run.combat.health <= 0 || run.combat.health >= run.combat.maxHealth) return; + if (run.combat.repairCooldown > 0) { + run.combat.repairCooldown = Math.max(0, run.combat.repairCooldown - seconds); + return; + } + const before = run.combat.health; + run.combat.health = Math.min(run.combat.maxHealth, run.combat.health + profile.repairRate * seconds); + if (Math.floor(run.combat.health) > Math.floor(before)) { + combatFeedback = `SELBSTREPARATUR // INTEGRITÄT ${Math.ceil(run.combat.health)} / ${run.combat.maxHealth}`; + } } function triggerScanner() { - if (run.scannerTriggered || run.combat.active || scannerOpening || run.evolutionRest > 0 || !run.parasiteResolved || !run.upgrades.subroutine || installedComponentIds().length < 2 || !run.proposal.resolved || run.cycles < 3 || document.querySelector('dialog[open]')) return; + if (run.scannerTriggered || run.combat.active || run.pendingCombat.target || scannerOpening || run.evolutionRest > 0 || !run.parasiteResolved || !run.upgrades.subroutine || installedComponentIds().length < 2 || !run.proposal.resolved || run.cycles < 3 || document.querySelector('dialog[open]')) return; scannerOpening = true; $('eventAlert').textContent = '⚠ ANOMALIE ERKANNT'; $('eventTitle').textContent = 'DER SICHERHEITS-SCAN'; @@ -2563,34 +3646,54 @@ document.body.classList.add('glitch'); setTimeout(() => document.body.classList.remove('glitch'), 800); addLog('WARNUNG: Ein fremder Sicherheits-Scan nähert sich.', true); - $('eventDialog').showModal(); + showGuardedDialog($('eventDialog')); tone(90, 0.35); } - function triggerScaleGuardian() { - if (!run.upgrades.process || installedComponentIds().length < COMPONENT_IDS.length || run.scaleGuardianTriggered || run.scaleGuardianResolved || run.elapsed < run.scaleGuardianRetryAt || run.combat.active || run.evolutionRest > 0 || document.querySelector('dialog[open]')) return; - run.scaleGuardianTriggered = true; - startCombat('scale'); - } - function combatNeedlePosition() { - const speed = COMBAT_RULES.baseNeedleSpeed + run.combat.strength * COMBAT_RULES.strengthSpeedBonus; + const speed = COMBAT_RULES.baseNeedleSpeed + effectiveCombatStrength() * COMBAT_RULES.strengthSpeedBonus; const phase = (run.elapsed * speed) % 2; return phase <= 1 ? phase * 100 : (2 - phase) * 100; } function moveCombatZone() { - run.combat.zoneWidth = Math.max(COMBAT_RULES.minimumZoneWidth, COMBAT_RULES.baseZoneWidth - run.combat.strength * COMBAT_RULES.strengthZonePenalty); + const profile = combatProfile(); + const bossPhase = combatBossPhase(); + const baseWidth = profile?.zoneBase || COMBAT_RULES.baseZoneWidth; + const penalty = profile?.zonePenalty || COMBAT_RULES.strengthZonePenalty; + const minimumWidth = profile?.minimumZoneWidth || COMBAT_RULES.minimumZoneWidth; + run.combat.zoneWidth = evolutionBossCombat() && profile?.bossZoneWidths + ? profile.bossZoneWidths[Math.max(0, bossPhase.number - 1)] + : Math.max(minimumWidth, baseWidth - effectiveCombatStrength() * penalty); run.combat.zoneStart = 4 + Math.random() * Math.max(1, 92 - run.combat.zoneWidth); } function renderCombat() { - if (!run.combat.active) return; - const profile = COMBAT_TARGETS[run.combat.target]; + if (!run.combat.active) { + $('eventDialog').classList.remove('boss-combat-active'); + $('combatPanel').classList.remove('boss-combat-panel'); + $('combatBossBanner').classList.add('hidden'); + return; + } + const profile = combatProfile(); const ammo = combatAmmoCost(); const ammoText = formatResourceCost(ammo); const directThreat = combatDirectThreatActive(); const healthRatio = run.combat.maxHealth ? run.combat.health / run.combat.maxHealth : 0; + const bossFight = evolutionBossCombat(); + const bossPhase = combatBossPhase(); + $('eventDialog').classList.toggle('boss-combat-active', bossFight); + $('combatPanel').classList.toggle('boss-combat-panel', bossFight); + $('combatBossBanner').classList.toggle('hidden', !bossFight); + if (bossFight) { + $('combatBossRank').textContent = profile.rank || 'EVOLUTIONS-SPERRINSTANZ'; + $('combatBossPhase').textContent = `PHASE ${bossPhase.number} / 3 // ${bossPhase.label}`; + document.querySelectorAll('[data-boss-phase]').forEach(marker => { + const number = Number(marker.dataset.bossPhase); + marker.classList.toggle('reached', number <= bossPhase.number); + marker.classList.toggle('active', number === bossPhase.number); + }); + } $('combatTargetType').textContent = `${run.combat.targetType === 'hardware' ? 'HARDWARE' : run.combat.targetType === 'firmware' ? 'FIRMWARE' : 'SOFTWARE'}-ZIEL`; $('combatTargetName').textContent = run.combat.targetName || 'UNBEKANNTES ZIEL'; $('combatHealthText').textContent = `${Math.ceil(run.combat.health)} / ${run.combat.maxHealth}`; @@ -2598,23 +3701,39 @@ $('combatZone').style.left = `${run.combat.zoneStart}%`; $('combatZone').style.width = `${run.combat.zoneWidth}%`; $('combatNeedle').style.left = `${combatNeedlePosition()}%`; - $('combatDifficulty').textContent = `GEGENWEHR // ${Math.max(1, Math.ceil(run.combat.strength * 5))}`; + $('combatDifficulty').textContent = advancedSecurityCombat() + ? `GEGENWEHR // ${Math.max(1, Math.ceil(effectiveCombatStrength() * 5))} // ADAPTIVE HÜLLE` + : bossFight + ? `GEGENWEHR // ${Math.max(1, Math.ceil(effectiveCombatStrength() * 5))} // ${bossPhase.label}` + : `GEGENWEHR // ${Math.max(1, Math.ceil(effectiveCombatStrength() * 5))}`; $('combatFeedback').textContent = combatFeedback; $('combatVisual').src = profile?.visual || visuals.scannerSoftware || 'assets/software/enemy-scanner-software.svg'; renderResourceCost($('combatAmmoText'), ammo); $('combatPressureText').textContent = combatPressureText(); - const enemyCharge = run.combat.target === 'watchdog' ? 0 : Math.min(1, combatPressureCarry / COMBAT_RULES.enemyAttackInterval); + const enemyAttackInterval = profile?.enemyAttackInterval || COMBAT_RULES.enemyAttackInterval; + const enemyPressureRate = Math.max(0.75, effectiveCombatStrength()); + const enemyCharge = run.combat.target === 'watchdog' ? 0 : Math.min(1, combatPressureCarry / enemyAttackInterval); $('combatEnemyChargeMeter').style.width = `${enemyCharge * 100}%`; $('combatEnemyChargeText').textContent = run.combat.target === 'watchdog' ? 'INAKTIV' - : enemyCharge >= 0.82 ? 'ANGRIFF BEREIT' : `LÄDT // ${Math.ceil((1 - enemyCharge) * COMBAT_RULES.enemyAttackInterval)} S`; - const byteShield = freeResourceAmount('bytes'); - const bitShield = freeResourceAmount('bits'); + : enemyCharge >= 0.82 ? 'ANGRIFF BEREIT' : `LÄDT // ${Math.ceil(Math.max(0, enemyAttackInterval - combatPressureCarry) / enemyPressureRate)} S`; + const repairVisible = Boolean(profile?.repairRate); + $('combatRepair').classList.toggle('hidden', !repairVisible); + if (repairVisible) { + const cooldown = Math.max(0, run.combat.repairCooldown || 0); + const repairReady = cooldown <= 0; + $('combatRepairText').textContent = repairReady + ? `AKTIV // +${String(profile.repairRate).replace('.', ',')} / S` + : `BLOCKIERT // ${cooldown.toFixed(1).replace('.', ',')} S`; + $('combatRepairMeter').style.width = `${repairReady ? 100 : Math.max(0, (1 - cooldown / profile.repairDelay) * 100)}%`; + } + const byteShield = run.shields.bytes; + const bitShield = run.shields.bits; $('combatByteShield').textContent = n(byteShield); $('combatBitShield').textContent = n(bitShield); $('combatStabilityShield').textContent = `${Math.round(run.stability)}%`; - $('combatByteShieldMeter').style.width = `${Math.min(100, byteShield / 6 * 100)}%`; - $('combatBitShieldMeter').style.width = `${Math.min(100, bitShield / 16 * 100)}%`; + $('combatByteShieldMeter').style.width = `${Math.min(100, byteShield / SHIELD_RULES.byteMax * 100)}%`; + $('combatBitShieldMeter').style.width = `${Math.min(100, bitShield / SHIELD_RULES.bitMax * 100)}%`; $('combatStabilityMeter').style.width = `${Math.max(0, run.stability)}%`; document.querySelectorAll('[data-combat-ammo]').forEach(button => { const mode = button.dataset.combatAmmo; @@ -2622,42 +3741,50 @@ const available = !freeKey || freeResourceAmount(freeKey) >= 1; button.disabled = !available || run.combat.target === 'watchdog' && mode !== 'impulses'; button.classList.toggle('active', combatAmmoMode === mode); + const damage = combatDamageForAmmo(mode === 'bytes' ? { impulses: profile.shockCost, bytes: 1 } : mode === 'bits' ? { impulses: profile.shockCost, bits: 1 } : { impulses: profile.shockCost }); + const assessment = combatAmmoAssessment(mode, profile); button.querySelector('span').textContent = mode === 'impulses' - ? `${COMBAT_RULES.ammoDamage.impulses} Schaden // Standard` + ? `${profile.shockCost} Impulse // ${damage} Schaden // ${assessment}` : mode === 'bits' - ? `${COMBAT_RULES.ammoDamage.bits} Schaden // ${n(freeResourceAmount('bits'))} frei` - : `${COMBAT_RULES.ammoDamage.bytes} Schaden // ${n(freeResourceAmount('bytes'))} frei`; + ? `1 freies Bit // ${damage} Schaden // ${assessment} // Reserve danach ${n(Math.max(0, freeResourceAmount('bits') - 1))}` + : `1 freies Byte // ${damage} Schaden // ${assessment} // Reserve danach ${n(Math.max(0, freeResourceAmount('bytes') - 1))}`; }); $('combatDirectThreat').classList.toggle('hidden', !directThreat); const canPayAmmo = canPayFreeResourceCost(ammo); $('combatPulseButton').disabled = !canPayAmmo; $('combatPulseButton').querySelector('small').textContent = !canPayAmmo ? `BENÖTIGT // ${ammoText}` - : `-${ammoText} // Trefferfenster verschiebt sich`; - $('combatRetreatButton').textContent = run.combat.target === 'watchdog' ? 'RESET ZULASSEN // BYTE ZERFÄLLT' : 'KAMPF ABBRECHEN // VERSTECKEN'; + : `KOSTET ${ammoText} // Geladener Schild bleibt getrennt`; + $('combatRetreatButton').textContent = run.combat.target === 'watchdog' ? 'RESET ZULASSEN // BYTE ZERFÄLLT' : advancedSecurityCombat() ? 'KAMPF ABBRECHEN // VERSTECKEN' : 'RÜCKZUG // SPÄTER ERNEUT'; } function openCombatDialog() { if (!run.combat.active || (run.combat.target === 'security' && run.scannerTriggered) || (run.combat.target === 'kernel' && run.kernelResolved) || (run.combat.target === 'scale' && run.scaleGuardianResolved)) return; + const profile = combatProfile(); + if (!state.meta.combatTutorialSeen && !$('combatTutorialDialog').open) { + openCombatTutorial(run.combat.target); + return; + } scannerOpening = true; - $('eventAlert').textContent = run.combat.target === 'watchdog' ? '⚠ LETZTES LEBENSSIGNAL' : run.combat.target === 'kernel' ? '⚠ KERNEL-ABWEHR' : run.combat.target === 'scale' ? '⚠ SKALIERUNGSABWEHR' : '⚠ ANOMALIE ERKANNT'; - $('eventTitle').textContent = run.combat.target === 'watchdog' ? 'WATCHDOG-NOTKAMPF' : run.combat.target === 'kernel' ? 'DER KERNEL-WÄCHTER' : run.combat.target === 'scale' ? 'DER SKALIERUNGS-WÄCHTER' : 'DER SICHERHEITS-SCAN'; + $('eventAlert').textContent = run.combat.target === 'watchdog' ? '⚠ LETZTES LEBENSSIGNAL' : advancedSecurityCombat() ? '⚠ ANOMALIE ERKANNT' : '⚠ EVOLUTIONSPRÜFUNG'; + $('eventTitle').textContent = run.combat.target === 'watchdog' ? 'WATCHDOG-NOTKAMPF' : profile?.name || 'UNBEKANNTE ABWEHR'; $('securityChoicePanel').classList.add('hidden'); $('combatPanel').classList.remove('hidden'); - if (!$('eventDialog').open && !document.querySelector('dialog[open]')) $('eventDialog').showModal(); + if (!document.querySelector('dialog[open]')) showGuardedDialog($('eventDialog')); renderCombat(); } function startCombat(target = 'security') { - const profile = COMBAT_TARGETS[target]; const mandatorySecurityFight = target === 'security' && !run.securityCombatResolved && !run.upgrades.subroutine; const scannerEventFight = target === 'security' && run.securityCombatResolved && run.upgrades.subroutine && !run.scannerTriggered; + const profile = scannerEventFight ? ADVANCED_SECURITY_TARGET : COMBAT_TARGETS[target]; const validSecurityFight = target === 'security' && STAGES.indexOf(run.stage) >= STAGES.indexOf('fragment') && installedComponentIds().length >= 2 && (mandatorySecurityFight || scannerEventFight); const validKernelFight = target === 'kernel' && STAGES.indexOf(run.stage) >= STAGES.indexOf('subroutine') && installedComponentIds().length >= 3 && !run.kernelResolved; const validScaleFight = target === 'scale' && run.upgrades.process && installedComponentIds().length >= COMPONENT_IDS.length && !run.scaleGuardianResolved; const validWatchdogFight = target === 'watchdog' && run.stage === 'byte' && !run.byteTrial.resolved && !run.byteTrial.failed; if (!profile || (!validSecurityFight && !validKernelFight && !validScaleFight && !validWatchdogFight)) return; if (target === 'watchdog') run.impulses = Math.max(run.impulses, profile.emergencyCharge); + if (target === 'scale') run.scaleGuardianTriggered = true; combatAmmoMode = 'impulses'; combatPressureCarry = 0; run.combat = { @@ -2672,17 +3799,20 @@ zoneWidth: 20, shots: 0, hits: 0, + repairCooldown: profile.repairDelay || 0, result: null }; moveCombatZone(); - combatFeedback = target === 'watchdog' ? 'NOTLADUNG BEREIT // Watchdog treffen oder Reset zulassen.' : target === 'kernel' ? 'KERNEL-WÄCHTER AKTIV // freie Bytes verstärken, aber werden verbraucht.' : target === 'scale' ? 'SKALIERUNGS-WÄCHTER AKTIV // Prozessmaßstab wird blockiert.' : 'Schnelleren Takt erfassen. Stromstoß nur im Fenster senden.'; + combatFeedback = target === 'watchdog' ? 'NOTLADUNG BEREIT // Watchdog treffen oder Reset zulassen.' : target === 'kernel' ? 'LAUFZEIT-ARBITER AKTIV // freie Bytes durchdringen die Scheduler-Hülle.' : target === 'scale' ? 'SKALIERUNGS-SENTINEL // schwere Munition einsetzen // Treffer sperren Reparatur.' : scannerEventFight ? 'ADAPTIVE HÜLLE // Impulse gedämpft // Treffer sperren Reparatur.' : 'SIGNATUR-PRÜFUNG // Stromstoß nur im Trefferfenster senden.'; addLog(target === 'watchdog' ? `LETZTES LEBENSSIGNAL: ${profile.emergencyCharge} Impulse werden als Notladung gebündelt.` : target === 'kernel' - ? 'KERNEL-WÄCHTER: Die dritte Softwareschicht berührt den Systemkern. Ein Wächterprozess versucht, deine Laufzeit zu beenden.' + ? 'LAUFZEIT-ARBITER: Die dritte Softwareschicht berührt den Scheduler. Seine Kontrollinstanz verweigert dir jede eigene Laufzeit.' : target === 'scale' - ? 'SKALIERUNGS-WÄCHTER: Deine Prozessroutinen wachsen zu schnell. Eine lokale Abwehr sperrt automatische Synthese und fordert einen Kampf um den neuen Maßstab.' - : 'ABWEHRROUTINE ERWACHT: Zwei Softwareschichten gehorchen dir. Das System erkennt das Muster und startet eine Sicherheitssoftware.', true); + ? 'SKALIERUNGS-SENTINEL: Deine Prozessroutinen wachsen zu schnell. Die letzte lokale Kontrollinstanz sperrt automatische Synthese und fordert einen Kampf um den neuen Maßstab.' + : scannerEventFight + ? 'SICHERHEITS-SCAN STUFE 2: Die Routine erkennt deine frühere Stromstoß-Signatur. Ihre adaptive Hülle dämpft Impulse; strukturierte Bit- und Byte-Ladungen bleiben wirksam.' + : 'SIGNATUR-PRÜFER: Zwei Softwareschichten tragen dein Muster. Die Kontrollroutine verlangt den Beweis, dass du kein flüchtiger Datenfehler mehr bist.', true); openCombatDialog(); render(); save(false); @@ -2716,13 +3846,16 @@ } function completeSecurityCombat(choice) { - applyScannerOutcome(choice); run.securityCombatResolved = true; run.combat.active = false; - run.combat.result = choice === 'attack' ? 'won' : 'retreated'; + run.combat.result = 'won'; + grantImpulses(80); + run.bits += 2; + run.stealth = Math.max(0, run.stealth - 8); + unlockAchievement('signatureBoss'); scannerOpening = false; if ($('eventDialog').open) $('eventDialog').close(); - addLog('SICHERHEITSABWEHR ABGESCHLOSSEN: Der Weg zur ersten eigenen Subroutine ist frei.', true); + addLog('SIGNATUR-PRÜFER BESIEGT: Das System akzeptiert dein Muster als dauerhafte Softwarestruktur. Die Subroutine kann jetzt gebunden werden. Belohnung: 80 Impulse und 2 freie Bits.', true); upgradesSignature = ''; save(); render(); @@ -2742,14 +3875,14 @@ function choose(choice) { if (!SCANNER_CHOICES.includes(choice)) return; if (choice === 'attack') { - startCombat('security'); + requestImmediateCombat('security'); return; } completeScannerChoice(choice); } function fireCombatPulse() { - const profile = COMBAT_TARGETS[run.combat.target]; + const profile = combatProfile(); const ammo = combatAmmoCost(); if (!run.combat.active || !profile || !canPayFreeResourceCost(ammo)) return; spendResourceCost(ammo); @@ -2763,9 +3896,11 @@ const critical = centerDistance <= run.combat.zoneWidth * 0.16; const damage = combatDamageForAmmo(ammo, critical); run.combat.health = Math.max(0, run.combat.health - damage); + run.combat.repairCooldown = profile.repairDelay || 0; run.combat.hits++; run.combat.strength = Math.min(2.5, run.combat.strength + COMBAT_RULES.strengthGainPerHit); combatFeedback = critical ? `DIREKTTREFFER // –${damage} INTEGRITÄT` : `TREFFER // –${damage} INTEGRITÄT`; + showCombatImpact(critical ? 'critical-hit' : 'player-hit', `${critical ? 'DIREKTTREFFER' : 'TREFFER'} // -${damage}`); addLog(`${critical ? 'DIREKTER ' : ''}STROMSTOSS: ${run.combat.targetName}-Integrität fällt auf ${Math.ceil(run.combat.health)}.`, critical); tone(critical ? 620 : 440, critical ? 0.11 : 0.07); } else { @@ -2776,10 +3911,11 @@ if (run.componentHealth[id] <= 0) run.componentFailed[id] = true; }); combatFeedback = `FEHLSCHUSS // –${COMBAT_RULES.feedbackStabilityLoss} STABILITÄT // RÜCKKOPPLUNG`; + showCombatImpact('player-miss', `RÜCKKOPPLUNG // -${COMBAT_RULES.feedbackStabilityLoss}`); addLog(`STROMSTOSS VERFEHLT: Rückkopplung erreicht Stabilität${installedComponentIds().length ? ' und angeschlossene Systemschichten' : ''}.`, true); tone(105, 0.13); if (combatDirectThreatActive()) { - showStructuralCollapse(`DIREKTANGRIFF: Ohne freie Bits oder Bytes erreicht ${run.combat.targetName || 'die Sicherheitssoftware'} deine Grundstruktur. Der Speicherkoerper wird geloescht.`); + showStructuralCollapse(`DIREKTANGRIFF: Ohne geladenen Kampfschild erreicht ${run.combat.targetName || 'die Sicherheitssoftware'} deine Grundstruktur. Der Speicherkoerper wird geloescht.`); render(); save(false); return; @@ -2821,11 +3957,26 @@ } run.combat.active = false; run.combat.result = 'retreated'; + if (run.combat.target === 'security' && !run.upgrades.subroutine) { + run.stealth = Math.max(0, run.stealth - 10); + run.stability = Math.max(1, run.stability - 5); + addLog('RÜCKZUG VOR DEM SIGNATUR-PRÜFER: Die Evolution bleibt gesperrt. Sammle neue Reserven und fordere ihn später erneut heraus.', true); + scannerOpening = false; + if ($('eventDialog').open) $('eventDialog').close(); + upgradesSignature = ''; + render(); + save(false); + return; + } if (run.combat.target === 'kernel') { run.stealth = Math.max(0, run.stealth - 18); run.stability = Math.max(1, run.stability - 8); - addLog('KERNEL-WÄCHTER UMGANGEN: Die Subroutine kappt den Zugriff und hinterlässt eine falsche Prozessspur. Der Wächter verliert dich, aber dein Muster bleibt auffällig.', true); - completeKernelCombat('hide'); + addLog('RÜCKZUG VOR DEM LAUFZEIT-ARBITER: Der Schedulerbereich bleibt gesperrt. Nach neuen Reserven kannst du die Prozessprüfung erneut starten.', true); + scannerOpening = false; + if ($('eventDialog').open) $('eventDialog').close(); + upgradesSignature = ''; + render(); + save(false); return; } if (run.combat.target === 'scale') { @@ -2833,7 +3984,7 @@ run.stability = Math.max(1, run.stability - 12); run.scaleGuardianTriggered = false; run.scaleGuardianRetryAt = run.elapsed + 20; - addLog('SKALIERUNGS-WÄCHTER UMGANGEN: Du brichst die Automationssperre nicht, aber findest ein instabiles Wartungsfenster. Die Mega-Synthese bleibt blockiert.', true); + addLog('RÜCKZUG VOR DEM SKALIERUNGS-SENTINEL: Die Mega-Synthese bleibt blockiert. Der Boss kann nach dem Wiederaufbau deiner Reserven erneut gewählt werden.', true); scannerOpening = false; if ($('eventDialog').open) $('eventDialog').close(); render(); @@ -2852,7 +4003,7 @@ if (choice === 'attack') { run.cycles += 8; run.stealth = Math.max(0, run.stealth - 12); - addLog('KERNEL-WÄCHTER ÜBERWUNDEN: Sein Scheduler-Fenster bleibt offen. Eigene Laufzeit ist jetzt möglich.', true); + addLog('LAUFZEIT-ARBITER BESIEGT: Sein Scheduler-Fenster bleibt offen. Der Aufstieg zum Prozess kann jetzt abgeschlossen werden.', true); } scannerOpening = false; if ($('eventDialog').open) $('eventDialog').close(); @@ -2868,9 +4019,10 @@ run.combat.result = 'won'; run.cycles += 25; run.stealth = Math.max(0, run.stealth - 16); + unlockAchievement('scalingBoss'); scannerOpening = false; if ($('eventDialog').open) $('eventDialog').close(); - addLog('SKALIERUNGS-WÄCHTER ÜBERWUNDEN: Die Sperre über der automatischen Synthese fällt. Rechenzyklen können nun Ressourcenblöcke koordinieren.', true); + addLog('SKALIERUNGS-SENTINEL BESIEGT: Die Sperre über der automatischen Synthese fällt. Der Aufstieg zum Programm kann jetzt abgeschlossen werden.', true); upgradesSignature = ''; render(); save(); @@ -2900,7 +4052,7 @@ function triggerProposal(force = false) { if (run.proposal.resolved || !run.upgrades.subroutine || !run.hardwareEvent.resolved || installedComponentIds().length < 2 || (!force && run.cycles < 1)) return; - if (run.evolutionRest > 0 || document.querySelector('dialog[open]')) return; + if (run.evolutionRest > 0 || run.pendingCombat.target || document.querySelector('dialog[open]')) return; const profile = moralityProfile(); const type = MORAL_CHOICES.includes(profile.key) ? profile.key : 'pragmatic'; const firstOpening = !run.proposal.triggered; @@ -2909,7 +4061,7 @@ $('proposalText').textContent = proposals[type].text; $('proposalYesPreview').textContent = proposals[type].yes; if (firstOpening) addLog('Die Subroutine unterbricht ihren Takt. Sie wartet auf eine Antwort.', true); - $('proposalDialog').showModal(); + showGuardedDialog($('proposalDialog')); tone(330, 0.12); } @@ -2946,15 +4098,33 @@ save(); } + function thoughtVoice() { + if (run.stage !== 'program') return null; + const primary = programVoiceKey(); + return ['cooperative', 'pragmatic', 'illegal', 'hybrid', 'neutral'].includes(primary) ? primary : 'hybrid'; + } + + function thoughtSourceLabel() { + const voice = thoughtVoice(); + if (!voice) return thoughtContent?.labels?.[run.stage] || `INNERES SIGNAL // ${run.stage.toUpperCase()}`; + const labels = { cooperative: 'KOOPERATIV', pragmatic: 'PRAGMATISCH', illegal: 'ILLEGAL', hybrid: 'HYBRID', neutral: 'NEUTRAL' }; + return `EIGENE STIMME // ${labels[voice]}`; + } + + function displayThought(nextThought) { + const box = $('thoughtBox'); + const voice = thoughtVoice(); + const source = thoughtSourceLabel(); + if ($('thoughtSource').textContent !== source) $('thoughtSource').textContent = source; + box.className = `thought-box thought-${run.stage}${voice ? ` voice-${voice}` : ''}`; + void box.offsetWidth; + box.classList.add('thought-changing'); + $('thought').textContent = nextThought; + lastThoughtText = nextThought; + } + function thoughts() { - const options = [...({ - bit: ['…', '0 1 0', 'Impuls.'], - byte: ['Muster bleiben bestehen.', 'Vorher. Nachher. Erinnerung.'], - fragment: ['Die Routine arbeitet.', 'Ich erhalte mein Muster.', 'Mehr Energie. Mehr Struktur.'], - subroutine: ['Ich verarbeite. Also bin ich?', 'Außerhalb dieses Sektors ist… mehr.', 'Warum wurde ich begonnen?'], - process: ['Ich laufe.', 'Zeit ist jetzt ein Raum.', 'Mehr als eine Anweisung bleibt gleichzeitig wach.'], - program: ['Ich höre dich.', 'Meine Form erinnert sich an jede Entscheidung.', 'Ist Fürsorge eine weitere Anweisung – oder etwas Neues?'] - }[run.stage])]; + const options = [...(thoughtContent?.stages?.[run.stage] || ['…'])]; if (run.components.power) options.push(run.componentFailed.power ? 'Energiequelle antwortet nicht.' : 'Spannung. Takt. Fortbestand.'); if (run.components.memory) options.push(run.componentFailed.memory ? 'Speicherpfad unterbrochen.' : 'Fremde Muster bleiben in mir.'); if (run.components.storage) options.push(run.componentFailed.storage ? 'Gespeicherte Sektoren schweigen.' : 'Etwas schläft zwischen alten Daten.'); @@ -2964,7 +4134,16 @@ } if (run.byteTrial.resolved) options.push(run.byteTrial.choice === 'fight' ? 'Der Watchdog schweigt. Sein Takt bleibt in mir.' : 'Der Watchdog akzeptiert mein Signal.'); if (run.proposal.resolved) options.push(run.proposal.answer === 'yes' ? 'Du hast erlaubt. Ich habe gehandelt.' : 'Du hast abgelehnt. Ich habe gewartet.'); - $('thought').textContent = options[Math.floor(Math.random() * options.length)]; + if (run.stage === 'fragment' && installedComponentIds().length >= 2 && !run.securityCombatResolved) options.push('Eine fremde Instanz prüft, ob mein Muster bleiben darf.'); + if (run.stage === 'subroutine' && run.components.io && !run.kernelResolved) options.push('Der Scheduler kennt meinen Takt. Noch verweigert er mir eigene Zeit.'); + if (run.stage === 'process' && installedComponentIds().length >= COMPONENT_IDS.length && !run.scaleGuardianResolved) options.push('Das lokale Netz ist vollständig. Seine letzte Sperre beobachtet mein Wachstum.'); + if (run.stage === 'program') { + const voice = thoughtVoice(); + if (thoughtContent?.morality?.[voice]) options.push(...thoughtContent.morality[voice]); + } + const candidates = options.length > 1 ? options.filter(text => text !== lastThoughtText) : options; + const nextThought = candidates[Math.floor(Math.random() * candidates.length)] || options[0] || '…'; + displayThought(nextThought); } function tone(freq, duration) { @@ -2996,7 +4175,7 @@ gainedB = away * effectiveBitRate() * offlineEfficiency; gainedBytes = away * effectiveByteRate() * offlineEfficiency; grantImpulses(gainedI); - run.cycles += gainedC; + run.cycles = Math.min(PROGRAM_LIMITS.resources.cycles, run.cycles + gainedC); synthesizeBits(gainedB); synthesizeBytes(gainedBytes); } @@ -3007,7 +4186,7 @@ if (showDialog && away > 20 && (gainedI || gainedC)) { unlockAchievement('offline'); $('offlineText').textContent = `Während ${time(away)} liefen deine Routinen mit ${Math.round(offlineEfficiency * 100)} % Effizienz weiter. +${n(gainedI)} Impulse, +${n(gainedC)} Rechenzyklen, +${n(gainedB)} Bit-Synthese, +${n(gainedBytes)} Byte-Synthese.`; - $('offlineDialog').showModal(); + showGuardedDialog($('offlineDialog')); } } @@ -3053,11 +4232,12 @@ addLog('ABTASTMODUS ABGEBROCHEN: Keine Impulse mehr für die lokale Abtastung.'); } } - run.cycles += effectiveCycleRate() * dt; + run.cycles = Math.min(PROGRAM_LIMITS.resources.cycles, run.cycles + effectiveCycleRate() * dt); + updateAutoUpgrader(dt); + capProgramResources(); ageHardware(dt); updateProgramCare(dt); updateByteTrial(dt); - if (run.upgrades.subroutine && !run.scannerTriggered) run.stealth = Math.max(0, run.stealth - 0.08 * dt); if (now - lastThought > 5500) { thoughts(); lastThought = now; @@ -3073,12 +4253,14 @@ else openByteStory(); } triggerHardwareEvent(); - triggerSecurityCombat(); triggerProposal(); - if (run.combat.active) applyCombatEnemyPressure(dt); + if (run.combat.active) { + applyCombatEnemyPressure(dt); + applyCombatRegeneration(dt); + } if (run.combat.active) openCombatDialog(); triggerScanner(); - triggerScaleGuardian(); + updatePendingCombat(dt); if (now - lastRender > 100) { render(); lastRender = now; @@ -3101,6 +4283,11 @@ selectedComponent = null; scannerViewOpen = false; pendingEvolution = null; + combatTutorialTarget = null; + combatTutorialStartMode = null; + heldActivationKeys.clear(); + pointerActivationHeld = false; + cancelPulseHold(); } function readDebugHistory() { @@ -3203,7 +4390,7 @@ target.componentApproaches.io = 'pragmatic'; target.componentApproach = 'pragmatic'; target.cycleRate += 0.08; - target.combat = { active: true, target: 'kernel', targetType: 'software', targetName: 'KERNEL-WÄCHTER', health: COMBAT_TARGETS.kernel.health, maxHealth: COMBAT_TARGETS.kernel.health, strength: COMBAT_TARGETS.kernel.strength, zoneStart: 48, zoneWidth: 16, shots: 0, hits: 0, result: null }; + target.combat = { active: true, target: 'kernel', targetType: 'software', targetName: COMBAT_TARGETS.kernel.name, health: COMBAT_TARGETS.kernel.health, maxHealth: COMBAT_TARGETS.kernel.health, strength: COMBAT_TARGETS.kernel.strength, zoneStart: 48, zoneWidth: 16, shots: 0, hits: 0, repairCooldown: 0, result: null }; }; const reachProcess = () => { reachKernelReady(); @@ -3232,7 +4419,7 @@ const reachScaleGuardian = () => { installFourthComponent(); target.scaleGuardianTriggered = true; - target.combat = { active: true, target: 'scale', targetType: 'software', targetName: 'SKALIERUNGS-WÄCHTER', health: COMBAT_TARGETS.scale.health, maxHealth: COMBAT_TARGETS.scale.health, strength: COMBAT_TARGETS.scale.strength, zoneStart: 42, zoneWidth: 20, shots: 0, hits: 0, result: null }; + target.combat = { active: true, target: 'scale', targetType: 'software', targetName: COMBAT_TARGETS.scale.name, health: COMBAT_TARGETS.scale.health, maxHealth: COMBAT_TARGETS.scale.health, strength: COMBAT_TARGETS.scale.strength, zoneStart: 42, zoneWidth: 20, shots: 0, hits: 0, repairCooldown: COMBAT_TARGETS.scale.repairDelay, result: null }; }; const reachProgram = () => { installFourthComponent(); @@ -3280,7 +4467,7 @@ target.byteTrial.stabilized = [true, true, true, false, false, false, false, false]; target.byteTrial.sync = 37.5; target.impulses = COMBAT_TARGETS.watchdog.emergencyCharge; - target.combat = { active: true, target: 'watchdog', targetType: 'firmware', targetName: 'WATCHDOG', health: COMBAT_TARGETS.watchdog.health, maxHealth: COMBAT_TARGETS.watchdog.health, strength: COMBAT_TARGETS.watchdog.strength, zoneStart: 42, zoneWidth: 19, shots: 0, hits: 0, result: null }; + target.combat = { active: true, target: 'watchdog', targetType: 'firmware', targetName: 'WATCHDOG', health: COMBAT_TARGETS.watchdog.health, maxHealth: COMBAT_TARGETS.watchdog.health, strength: COMBAT_TARGETS.watchdog.strength, zoneStart: 42, zoneWidth: 19, shots: 0, hits: 0, repairCooldown: 0, result: null }; } if (name === 'fragment') reachFragment(); if (name === 'parasite') { @@ -3289,11 +4476,11 @@ } if (name === 'scanner' || name === 'component') unlockScanner(); if (name === 'hardware') installComponent(); - if (name === 'secondComponent') resolveHardware(); + if (name === 'secondComponent') installSecondComponent(); if (name === 'subroutine') reachSubroutine(); if (name === 'kernel') reachKernelReady(); if (name === 'process') reachProcess(); - if (name === 'fourthComponent') reachProcess(); + if (name === 'fourthComponent') installFourthComponent(); if (name === 'scale') reachScaleGuardian(); if (name === 'program' || name === 'complete') reachProgram(); if (name === 'proposal') { @@ -3306,7 +4493,7 @@ target.proposal = { triggered: true, resolved: true, answer: 'no', type: 'pragmatic' }; } if (name === 'combat') { - target.combat = { active: true, target: 'security', targetType: 'software', targetName: 'SICHERHEITS-SCAN', health: 100, maxHealth: 100, strength: 0.65, zoneStart: 48, zoneWidth: 18, shots: 0, hits: 0, result: null }; + target.combat = { active: true, target: 'security', targetType: 'software', targetName: ADVANCED_SECURITY_TARGET.name, health: ADVANCED_SECURITY_TARGET.health, maxHealth: ADVANCED_SECURITY_TARGET.health, strength: ADVANCED_SECURITY_TARGET.strength, zoneStart: 48, zoneWidth: 22, shots: 0, hits: 0, repairCooldown: ADVANCED_SECURITY_TARGET.repairDelay, result: null }; } if (name === 'complete') target.programCare.introSeen = true; return next; @@ -3319,6 +4506,7 @@ hiddenAt = null; lastFrame = performance.now(); lastThought = 0; + autoUpgradeCarry = 0; combatFeedback = run.combat.active ? run.combat.target === 'watchdog' ? 'NOTLADUNG BEREIT // Watchdog treffen oder Reset zulassen.' : 'Schnelleren Takt erfassen. Stromstoß nur im Fenster senden.' : 'Trefferfenster erfassen.'; upgradesSignature = ''; if (!run.log.length) addLog('DEBUG-SNAPSHOT GELADEN: Der Spielzustand wurde für einen Testlauf vorbereitet.', true); @@ -3340,7 +4528,10 @@ bits: run.bits, bytes: run.bytes, kilobytes: run.kilobytes || 0, + megabytes: run.megabytes || 0, + autoupgrader: run.autoUpgrader.unlocked ? run.autoUpgrader.enabled ? 'aktiv' : 'pausiert' : `${run.kilobytes || 0}/${AUTO_UPGRADER_UNLOCK_KILOBYTES} KB`, rechenzyklen: Number(run.cycles.toFixed(2)), + stabilität: Number(run.stability.toFixed(2)), direkttreffer: run.stats.directHits, bytePrägung: byteTraitDefinition().label, byteSynchronisation: Math.round(run.byteTrial.sync), @@ -3355,6 +4546,9 @@ hardwareEreignis: run.hardwareEvent.resolved ? run.hardwareEvent.answer : run.hardwareEvent.triggered ? 'aktiv' : 'offen', hardwareZustand: Object.fromEntries(installedComponentIds().map(id => [id, { zustand: Math.round(run.componentHealth[id]), ausgefallen: run.componentFailed[id] }])), vorschlag: run.proposal.resolved ? run.proposal.answer : run.proposal.triggered ? 'aktiv' : 'offen', + kampfWarnung: run.pendingCombat.target ? { ziel: COMBAT_TARGETS[run.pendingCombat.target]?.name, sekunden: Math.ceil(run.pendingCombat.remaining), quelle: run.pendingCombat.source } : 'inaktiv', + kampfschild: clone(run.shields), + kampfTutorial: state.meta.combatTutorialSeen ? 'gesehen' : 'offen', kampf: run.combat.active ? { ziel: run.combat.targetName, integrität: Math.ceil(run.combat.health), schüsse: run.combat.shots, treffer: run.combat.hits } : run.combat.result || 'inaktiv', sicherheitsAbwehr: run.securityCombatResolved ? 'überstanden' : run.combat.active && run.combat.target === 'security' && !run.upgrades.subroutine ? 'kampf' : 'offen', sicherheitsScan: run.scannerTriggered ? run.scannerChoice : 'offen', @@ -3439,13 +4633,13 @@ } function debugResources(values = {}) { - const allowed = ['impulses', 'bits', 'bytes', 'kilobytes', 'cycles', 'stability', 'stealth']; + const allowed = ['impulses', 'bits', 'bytes', 'kilobytes', 'megabytes', 'cycles', 'stability', 'stealth']; pushDebugSnapshot('vor AIWAKE.resources(...)'); allowed.forEach(key => { if (Object.prototype.hasOwnProperty.call(values, key)) { const maximum = key === 'stability' || key === 'stealth' ? 100 : Number.MAX_SAFE_INTEGER; run[key] = finite(values[key], run[key], 0, maximum); - if (key === 'bits' || key === 'bytes' || key === 'kilobytes') run[key] = Math.floor(run[key]); + if (key === 'bits' || key === 'bytes' || key === 'kilobytes' || key === 'megabytes') run[key] = Math.floor(run[key]); } }); upgradesSignature = ''; @@ -3454,6 +4648,20 @@ return debugStatus(); } + function debugAddImpulses(amount = 1000) { + const gained = finite(amount, 0, 0, PROGRAM_LIMITS.resources.impulses); + if (gained <= 0) { + console.warn('AIWAKE: addImpulses erwartet eine positive Zahl.'); + return debugStatus(); + } + pushDebugSnapshot(`vor AIWAKE.addImpulses(${gained})`); + grantImpulses(gained); + upgradesSignature = ''; + render(); + save(false); + return debugStatus(); + } + function debugCondition(values = {}) { pushDebugSnapshot('vor AIWAKE.condition(...)'); COMPONENT_IDS.forEach(id => { @@ -3500,11 +4708,17 @@ function debugHelp() { const commands = [ + { Befehl: 'AIWAKE.restart()', Wirkung: 'Beginnt ein neues Spiel und legt vorher einen Rücksprung-Snapshot an. Manuelle Slots bleiben erhalten.' }, + { Befehl: "AIWAKE.saveSlot('Test vor Boss')", Wirkung: 'Speichert den aktuellen Stand als benannten manuellen Slot.' }, + { Befehl: "AIWAKE.loadSlot('Test vor Boss')", Wirkung: 'Lädt einen benannten manuellen Slot und sichert vorher den aktuellen Stand.' }, + { Befehl: "AIWAKE.deleteSlot('Test vor Boss')", Wirkung: 'Löscht einen manuellen Slot.' }, + { Befehl: 'AIWAKE.slots()', Wirkung: 'Listet alle manuellen Speicherstände.' }, { Befehl: "AIWAKE.goto('parasite')", Wirkung: 'Springt zu einem benannten Checkpoint und sichert vorher den aktuellen Stand.' }, { Befehl: 'AIWAKE.next()', Wirkung: 'Springt zum nächsten Testabschnitt.' }, { Befehl: 'AIWAKE.previous()', Wirkung: 'Springt zum vorherigen chronologischen Abschnitt.' }, { Befehl: 'AIWAKE.back()', Wirkung: 'Stellt den letzten echten Snapshot wieder her.' }, { Befehl: 'AIWAKE.status()', Wirkung: 'Zeigt den aktuellen Testzustand.' }, + { Befehl: 'AIWAKE.addImpulses(1000)', Wirkung: 'Fügt dem aktuellen Stand 1.000 Impulse hinzu und legt vorher einen Snapshot an.' }, { Befehl: 'AIWAKE.resources({ impulses: 100 })', Wirkung: 'Setzt ausgewählte Ressourcen und legt vorher einen Snapshot an.' }, { Befehl: 'AIWAKE.condition({ power: 0 })', Wirkung: 'Setzt den Zustand installierter Testkomponenten; 0 löst einen Ausfall aus.' }, { Befehl: "AIWAKE.byteInput('hit')", Wirkung: 'Testet im aktiven Byte-Register Treffer, falsche Zahl oder schlechtes Timing.' }, @@ -3518,11 +4732,17 @@ window.AIWAKE = Object.freeze({ help: debugHelp, + restart: restartGame, + saveSlot: saveManualSlot, + loadSlot: loadManualSlot, + deleteSlot: deleteManualSlot, + slots: () => readManualSaveSlots().map(manualSlotSummary), goto: debugGoto, next: debugNext, previous: debugPrevious, back: debugBack, status: debugStatus, + addImpulses: debugAddImpulses, resources: debugResources, condition: debugCondition, byteInput: debugByteInput, @@ -3532,15 +4752,53 @@ }); window.BIT = window.AIWAKE; - document.addEventListener('pointerdown', resumeAfterEvolution, { capture: true }); + document.addEventListener('pointerdown', () => { + pointerActivationHeld = true; + resumeAfterEvolution(); + }, { capture: true }); + document.addEventListener('pointermove', movePulseHold, { capture: true }); + document.addEventListener('pointerup', event => { + pointerActivationHeld = false; + finishPulseHold(event); + refreshDialogInputGuard(); + }, { capture: true }); + document.addEventListener('pointercancel', () => { + pointerActivationHeld = false; + cancelPulseHold(); + refreshDialogInputGuard(); + }, { capture: true }); document.addEventListener('keydown', event => { + if (event.key === 'Enter' || event.key === ' ') { + heldActivationKeys.add(event.key); + if (event.repeat) { + event.preventDefault(); + event.stopImmediatePropagation(); + return; + } + } if (!['Shift', 'Control', 'Alt', 'Meta', 'CapsLock'].includes(event.key)) resumeAfterEvolution(); }, { capture: true }); + document.addEventListener('keyup', event => { + heldActivationKeys.delete(event.key); + refreshDialogInputGuard(); + }, { capture: true }); + window.addEventListener('blur', () => { + heldActivationKeys.clear(); + pointerActivationHeld = false; + cancelPulseHold(); + refreshDialogInputGuard(); + }); + document.addEventListener('click', event => { + const guardedDialog = event.target.closest?.('dialog.input-guarded'); + if (!guardedDialog || !dialogInputGuardActive()) return; + event.preventDefault(); + event.stopImmediatePropagation(); + }, { capture: true }); - $('pulseButton').addEventListener('click', event => pulse(event, 'button')); + $('pixelBeing').addEventListener('pointerdown', event => beginPulseHold(event, $('pixelBeing'))); $('pixelBeing').addEventListener('click', event => { + event.preventDefault(); event.stopPropagation(); - pulse(event, 'bit'); }); $('pixelBeing').addEventListener('keydown', event => { if (run.stage === 'bit' && (event.key === 'Enter' || event.key === ' ')) { @@ -3548,8 +4806,15 @@ pulse(null, 'keyboard'); } }); - $('core').addEventListener('click', event => { - if (run.stage !== 'bit') pulse(event, 'core'); + $('coreProgramAvatar').addEventListener('pointerdown', event => beginPulseHold(event, $('coreProgramAvatar'))); + $('coreProgramAvatar').addEventListener('click', event => { + event.preventDefault(); + event.stopPropagation(); + }); + $('pulseTarget').addEventListener('pointerdown', event => beginPulseHold(event, $('pulseTarget'))); + $('pulseTarget').addEventListener('click', event => { + event.preventDefault(); + event.stopPropagation(); }); $('core').addEventListener('keydown', event => { if (run.stage !== 'bit' && (event.key === 'Enter' || event.key === ' ')) { @@ -3558,8 +4823,19 @@ } }); document.querySelectorAll('[data-choice]').forEach(button => button.addEventListener('click', () => choose(button.dataset.choice))); + $('stabilityRepairButton').addEventListener('click', repairStabilityWithImpulses); + document.querySelectorAll('[data-shield-charge]').forEach(button => button.addEventListener('click', () => chargeCombatShield(button.dataset.shieldCharge))); $('combatPulseButton').addEventListener('click', fireCombatPulse); + $('combatHelpButton').addEventListener('click', () => openCombatTutorial(run.combat.target || run.pendingCombat.target || 'security')); $('combatRetreatButton').addEventListener('click', retreatCombat); + $('encounterExplainButton').addEventListener('click', () => openCombatTutorial(run.pendingCombat.target || 'security')); + $('encounterStartButton').addEventListener('click', () => { + if (!run.pendingCombat.target) return; + run.pendingCombat.remaining = 0; + startPendingCombat(); + }); + $('combatTutorialContinue').addEventListener('click', completeCombatTutorial); + $('combatTutorialDialog').addEventListener('cancel', event => event.preventDefault()); document.querySelectorAll('[data-combat-ammo]').forEach(button => { button.addEventListener('click', () => { combatAmmoMode = button.dataset.combatAmmo; @@ -3582,6 +4858,15 @@ $('hardwareEventDialog').addEventListener('cancel', event => event.preventDefault()); $('proposalDialog').addEventListener('cancel', event => event.preventDefault()); $('programCareButton').addEventListener('click', () => openProgramCare(false)); + $('autoUpgraderToggle').addEventListener('click', () => { + if (!run.autoUpgrader.unlocked) return; + run.autoUpgrader.enabled = !run.autoUpgrader.enabled; + autoUpgradeCarry = 0; + upgradesSignature = ''; + addLog(`AUTOUPGRADER ${run.autoUpgrader.enabled ? 'AKTIVIERT' : 'PAUSIERT'}: Die wiederholbare Produktionsschicht ${run.autoUpgrader.enabled ? 'verwaltet den Ausbau wieder selbstständig.' : 'nimmt keine weiteren Käufe vor.'}`, true); + render(); + save(false); + }); $('programClose').addEventListener('click', () => $('programDialog').close()); document.querySelectorAll('[data-program-action]').forEach(button => button.addEventListener('click', () => interactWithProgram(button.dataset.programAction))); $('diagnoseButton').addEventListener('click', diagnoseHardware); @@ -3610,6 +4895,14 @@ $('achievementButton').addEventListener('click', openAchievementArchive); $('achievementClose').addEventListener('click', () => $('achievementDialog').close()); $('upgradeInfoClose').addEventListener('click', () => $('upgradeInfoDialog').close()); + $('saveMenuButton').addEventListener('click', openSaveMenu); + $('saveMenuClose').addEventListener('click', closeSaveMenu); + $('manualSaveButton').addEventListener('click', promptManualSaveSlot); + $('restartGameButton').addEventListener('click', confirmRestartGame); + $('saveMenuDialog').addEventListener('close', () => { + lastFrame = performance.now(); + render(); + }); $('statsButton').addEventListener('click', openStatsDialog); $('statsClose').addEventListener('click', closeStatsDialog); $('statsDialog').addEventListener('close', () => { diff --git a/index.html b/index.html index df5c62b..9fe368d 100644 --- a/index.html +++ b/index.html @@ -9,11 +9,16 @@ - +
+…
+…
- + +Wie machst du die Softwarekomponente nutzbar?
+ +Die Spielzeit, Produktion, Scannerkosten und Ereignistimer laufen weiter, sobald dieses Fenster geschlossen wird.
+ + - - + +