commit 46116f7f4b45bfbf15fba7b69129ed3505325ae4 Author: fedora_openclaw Date: Sun Jul 5 18:56:44 2026 +0200 Initial commit: DOOM x MATRIX System Failure text adventure Modulares Python-Textadventure mit Kampfsystem (Stagger, Bullet Time, Glory Kills, Hacking-Skill-Checks), Hub-Raum-Navigation, prozeduralen Encountern, 3 Charakterklassen, mehreren Enden und Story-Elementen (weißer Hase, Orakel, schwarze-Katze-Deja-vu-Glitch). diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a3f7737 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*.pyc +doom_matrix/saves/*.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..914c994 --- /dev/null +++ b/README.md @@ -0,0 +1,250 @@ +# DOOM × MATRIX: SYSTEM FAILURE + +Ein interaktives Text-Adventure in Python, das die Lore von **DOOM** und **The Matrix** zu einer gemeinsamen, düsteren Verschwörung verschmilzt: *Die Matrix läuft nicht neben der Hölle — sie läuft auf ihr.* + +> "What if the Matrix was just a very inefficient DOOM engine?" + +--- + +## Inhaltsverzeichnis + +- [Schnellstart](#schnellstart) +- [Story-Prämisse](#story-prämisse) +- [Charakterklassen](#charakterklassen) +- [Spielablauf & Layer](#spielablauf--layer) +- [Kampfsystem](#kampfsystem) +- [Hacking-System](#hacking-system) +- [Inventar & Gegenstände](#inventar--gegenstände) +- [Enden](#enden) +- [Easter Eggs & Cheat-Codes](#easter-eggs--cheat-codes) +- [Speicherstände & Loop-Twist](#speicherstände--loop-twist) +- [Kommandozeilen-Optionen](#kommandozeilen-optionen) +- [Projektstruktur](#projektstruktur) +- [Erweitern des Spiels](#erweitern-des-spiels) + +--- + +## Schnellstart + +**Voraussetzungen:** Python 3.9+ (getestet mit 3.13). Keine Pflicht-Abhängigkeiten — `colorama` wird automatisch genutzt, falls installiert (für saubere ANSI-Farben in der klassischen Windows-Konsole), ist aber optional. + +```bash +python doom-matrix-adventure.py +``` + +Optionale Flags: + +```bash +python doom-matrix-adventure.py --fast # überspringt alle Tipp-/Dramatik-Verzögerungen +python doom-matrix-adventure.py --seed=1337 # reproduzierbarer Run (fester Zufalls-Seed) +python doom-matrix-adventure.py --fast --seed=42 # beides kombiniert +``` + +Der Einstieg funktioniert auch direkt über das Paket: + +```bash +python -m doom_matrix.main +``` + +--- + +## Story-Prämisse + +Du wachst im **Construct** auf — dem neutralen Trainingsraum der Matrix. Morpheus Hayden begrüßt dich als *Doom Slayer*, doch bevor du dich orientieren kannst, schlägt Alarm: Die Maschinen haben etwas gebaut, das sie **HELL SERVER** nennen. + +Die zentrale Wendung: **Argent-Energie treibt die Matrix an.** Die Menschen in den Stasiskapseln sind keine Batterien — sie sind Seelen-Prozessoren, die Dämonen rendern. Die Hölle ist kein Ort neben der Simulation, sie ist deren Fundament. Der Doom Slayer selbst ist ein *Rogue Process*, ein Programm, das sich weigerte gelöscht zu werden — deshalb kann er die Regeln beider Welten brechen. + +Auf dem Weg begegnest du dem **weißen Hasen** (ein Fourth-Wall-Break-Moment direkt zu Spielbeginn), dem **Orakel** in ihrer Küche mitten im Matrix-Code, und erlebst einen echten *"Fehler in der Matrix"*: eine schwarze Katze, die exakt zweimal denselben Weg läuft — das untrügliche Zeichen dafür, dass gerade jemand den Code verändert. + +--- + +## Charakterklassen + +Zu Beginn wählst du eine von drei Klassen. Sie bestimmen Startwerte, Startwaffe und wie schnell deine **Corruption** steigt (relevant für die Enden): + +| Klasse | Fokus | HP | Armor | Focus | Hacking | Strength | Corruption-Rate | Startwaffe | +|---|---|---|---|---|---|---|---|---| +| **Slayer** | Doom — Kraft & Ausdauer | 120 | 20 | 60 | 2 | 8 | ×1.0 | Super Shotgun (20 Shells) | +| **Operator** | Matrix — Hacking & Mobilität | 85 | 10 | 100 | 9 | 3 | ×1.0 | Plasma Rifle (100 Cells) | +| **Anomalie** (Hybrid) | Ausgewogen, einzigartige Dialoge | 100 | 15 | 80 | 5 | 5 | ×2.0 | Super Shotgun + etwas Plasma | + +Zusätzlich wählst du zu Spielbeginn im Construct eine zweite Waffe (Super Shotgun oder Plasma Rifle) — unabhängig von der Klasse. + +--- + +## Spielablauf & Layer + +Das Spiel ist in **Layer** unterteilt, die den Fortschritt der Simulation widerspiegeln: + +| Layer | Ort | +|---|---| +| 4 | Mars UAC Base | +| 3 | The Matrix (Start) | +| 2 | Hell Server | +| 1 | The Architect's Core | +| 0 | *(geheim)* — nur im wahren Ende erreichbar | + +### Ablauf + +1. **Construct-Intro** — Aufwachen, Morpheus Hayden, Alarm, Waffenwahl. +2. **Weißer Hase** — ein Hologramm huscht vorbei. Folgst du ihm, öffnet sich kurz ein "Kaninchenbau" mit einem Blick auf die reale Terminal-Sitzung (inkl. echtem Systembenutzernamen) — das setzt die Weichen für das geheime Ende. +3. **Agent-Imp-Kampf** — dein erster Gegner, halb Agent, halb Dämon. +4. **Wegwahl** — drei Portale: Mars UAC Base, tiefer in die Matrix, oder direkt zum Hell Server (Schnellweg, aber weniger Beute). +5. **Mars UAC Base** (Hub) — Waffenkammer (BFG 10000 + rote Keycard), Kontrollzentrum (Hacking-Check → Crucible-Fragment), Waffentresor (benötigt rote Keycard), prozedural generierte Nebenräume. +6. **Matrix Deep** — die **schwarze Katze** (Deja-vu-Glitch), das **Orakel** (Dialogszene + Crucible-Fragment), Kampf gegen **Agent Smith** (Dialogwahl beeinflusst, ob er sich vervielfacht). +7. Mars UAC Base und Matrix Deep sind **beide** vor dem Hell Server erreichbar — welchen Weg du zuerst nimmst, der andere folgt automatisch danach. Nur die dritte Portal-Option ("Schwarzes Loch") überspringt beide. +8. **Hell Server** (Hub) — Altarraum (drittes Crucible-Fragment), prozedurale Kampf-/Beuteräume, Bosskampf gegen den **Cyberdemon**. +9. **Der Archi-Lord** (Endboss) — Dialogwahl beeinflusst Corruption und das spätere Ende; bei drei gesammelten Crucible-Fragmenten kannst du sie zum **Crucible Sword** verschmelzen. +10. **Ending** — abhängig von Entscheidungen, Corruption und ob du den weißen Hasen gefolgt bist plus alle drei Fragmente besitzt. + +--- + +## Kampfsystem + +Kämpfe laufen rundenbasiert mit vier (bzw. fünf) Aktionen ab: + +- **Schießen** — wählt eine verfügbare Waffe, Schaden hängt vom **Waffen-Dreieck** ab: + - Ballistisch (Shotgun, Fäuste) ist stark gegen `demon`, schwach gegen `program`. + - Plasma (Plasma Rifle, BFG) ist stark gegen `program`/`hybrid`, etwas schwächer gegen `demon`. + - Nahkampf (Crucible Sword) ist ausgeglichen, leicht im Vorteil gegen `hybrid`. +- **Bullet Time** — kostet 25 Focus, garantiert Ausweichen des nächsten Gegnerangriffs und einen kritischen Treffer (+50% Schaden). Reicht der Focus nicht, verpufft der Versuch als normaler (ungeschützter) Zug. +- **Glory Kill** — nur nutzbar, wenn ein Gegner **gestaggert** ist (seine Stagger-Leiste ist voll). Tötet ihn sofort und heilt dich um 25 HP. Ohne gestaggerten Gegner schlägt der Versuch fehl und du kassierst einen Konter. +- **Hack** — ein Skill-Check (`d20 + Hacking` gegen die Firewall des Gegners). Erfolg lässt dich einen Effekt wählen (Einfrieren, Waffen-Jam, HP-Leak, Gegner umdrehen). Misslingt der Hack, erleidest du Rückstoßschaden und sammelst eine **Trace-Marke** — bei drei Marken schickt das System Verstärkung in den Kampf. +- **Medkit benutzen** — erscheint nur, wenn du ein Medkit im Inventar hast. + +Boss-Gegner (Cyberdemon, Archi-Lord) haben mehrere **Phasen**: bei bestimmten HP-Schwellen werden sie aggressiver und ihre Stagger-Leiste wird zurückgesetzt. + +--- + +## Hacking-System + +Hacking ist nicht nur eine Kampfoption, sondern auch der Schlüssel zu Umgebungsrätseln: + +- **Kontrollzentrum (Mars Base):** ein Hack mit Firewall 10 öffnet ein verstecktes Fach mit einem Crucible-Fragment. +- **Schwarze-Katze-Glitch (Matrix Deep):** ein optionaler Hack (Firewall 12) lässt dich der Code-Bruchstelle nachspüren und ein Log-Fragment finden — kostet aber etwas Corruption. + +Erfolgschance: `Würfelwurf (1-20) + Hacking-Wert ≥ Firewall des Ziels`. + +--- + +## Inventar & Gegenstände + +| Item | Wirkung | +|---|---| +| Medkit | Heilt 30 HP | +| Armor-Shard | Gibt 15 Armor | +| Ammo-Pack | Füllt Munition aller Waffen auf | +| Backup-Pille | Einmaliges Auto-Revive bei tödlichem Treffer | +| Rote/Blaue/Gelbe Keycard | Öffnet entsprechend verriegelte Türen in Hub-Räumen | +| Crucible-Fragment | Sammle 3, um den Crucible Sword zu schmieden — und Voraussetzung für das geheime Ende | +| Praetor-Token | Skillpunkt (Health, Focus, Hacking oder Strength dauerhaft erhöhen) | +| Log-Datei | Lore-Fragment, kein Kampfeffekt | + +**Waffen:** Fäuste (unbegrenzt), Super Shotgun, Plasma Rifle, BFG 10000 (limitierte Spezialmunition), Crucible Sword (nur nach Verschmelzung aller 3 Fragmente). + +--- + +## Enden + +Welches Ende du bekommst, hängt von Corruption, deiner letzten Dialogwahl beim Archi-Lord und ob du den weißen Hasen gefolgt bist **und** alle drei Crucible-Fragmente besitzt: + +| Ende | Bedingung | +|---|---| +| **Wake Up** *(geheimes, wahres Ende)* | Weißem Hasen gefolgt **und** 3/3 Crucible-Fragmente. Layer 0 wird erreicht — ein Fourth-Wall-Break mit deinem echten Systembenutzernamen. | +| **Der Dunkle Pakt** | Corruption ≥ 60% — du wirst selbst zum neuen Dark Lord. | +| **Der neue Architekt** | Dialogwahl "Ich will die Kontrolle übernehmen" beim Archi-Lord. | +| **...mit Cheats (New Game+)** | Dialogwahl "Ich will einen neuen Spielstand" beim Archi-Lord. | +| **Rip and Tear** | Standard-Ende, falls keine der obigen Bedingungen zutrifft. | +| **Game Over → Bluepill** | Bei Tod: die blaue Pille schlucken → ironisches Büro-Ende. | +| **Game Over → Neu laden** | Bei Tod: Programm beenden. | + +> Hinweis zum geheimen Ende: Um alle 3 Fragmente zu sammeln, musst du **sowohl** die Mars UAC Base (Kontrollzentrum-Hack) **als auch** Matrix Deep (Orakel) besuchen, bevor du den Hell Server (Altarraum) betrittst — die dritte Portal-Option ("Schwarzes Loch") überspringt beide und macht das wahre Ende in diesem Run unerreichbar. + +--- + +## Easter Eggs & Cheat-Codes + +Cheat-Codes können jederzeit anstelle einer normalen Eingabe (z. B. im Kampfmenü oder in Hub-Räumen) eingegeben werden: + +| Code | Effekt | +|---|---| +| `IDDQD` | God Mode an/aus (kein Schaden mehr) | +| `IDKFA` | Alle Waffen + volle Munition | +| `thereisnospoon` | Senkt Corruption leicht | +| `xyzzy` | Klassischer Nichts-passiert-Scherz (Referenz auf *Colossal Cave Adventure*, 1977) | +| `doom` / `knee-deep` / `kneedeepinthedead` | Pixel-Nostalgie-Easter-Egg, gibt einen Bonus-Praetor-Token | + +Weitere versteckte Momente: +- Der weiße Hase erscheint nur **einmal**, direkt vor dem ersten Kampf. +- Die schwarze Katze läuft zweimal exakt gleich — ein bewusster "Fehler in der Matrix", kein Zufallsevent. +- Das Orakel reagiert unterschiedlich, je nachdem ob du dem Hasen gefolgt bist und wie hoch deine Corruption ist. + +--- + +## Speicherstände & Loop-Twist + +Nach jedem Spielende wird automatisch ein Speicherstand unter `doom_matrix/saves/autosave.json` abgelegt. Zusätzlich führt `doom_matrix/saves/meta.json` Buch über die Gesamtzahl deiner Durchläufe. + +Ab dem **zweiten** Durchlauf reagiert Morpheus Hayden mit einer subtil unterschiedlichen Zeile zu Spielbeginn — ein wachsender Hinweis darauf, dass diese Simulation schon öfter gelaufen ist, als dir bewusst ist. + +--- + +## Kommandozeilen-Optionen + +| Flag | Beschreibung | +|---|---| +| `--fast` | Deaktiviert alle Tipp-Animationen und dramatischen Pausen | +| `--seed=` | Fixiert den Zufalls-Seed für reproduzierbare Durchläufe | + +--- + +## Projektstruktur + +``` +Doom-Matrix/ +├── doom-matrix-adventure.py # Kompatibilitäts-Wrapper / Haupteinstieg +└── doom_matrix/ + ├── main.py # Alternativer Einstiegspunkt (python -m doom_matrix.main) + ├── engine/ + │ ├── ui.py # Farben, Textausgabe, Balken, Glitch-Effekte + │ ├── game.py # Titel-Bildschirm, Klassenwahl, Szenen-Dispatch + │ ├── save.py # JSON-Speicherstände, Meta-Statistiken, Loop-Twist + │ └── cheats.py # Cheat-Code-Handling + ├── entities/ + │ ├── player.py # Spieler-Stats, Klassen, Leveling, Corruption + │ ├── enemy.py # Datengetriebene Gegner-Klasse (Stagger-System) + │ └── items.py # Item/Waffen/Inventar-Klassen, Waffen-Dreieck + ├── systems/ + │ ├── combat.py # Kampf-Engine (Stagger, Bullet Time, Glory Kill) + │ └── hacking.py # Skill-Check-System, Trace-Marken + ├── world/ + │ ├── rooms.py # Room-/Hub-Klassen für Raum-Navigation + │ ├── generator.py # Prozedurale Raum-/Encounter-Generierung + │ ├── layers.py # Story-Szenen aller Layer (Intro bis Endboss) + │ └── endings.py # Ending-Logik und Game-Over-Verzweigung + ├── data/ + │ ├── weapons.json # Waffendaten (Schaden, Munitionstyp, Typ) + │ ├── enemies.json # Gegnerdaten (HP, Stagger, Angriff, Firewall) + │ └── items.json # Item-Definitionen + └── saves/ # Autosave & Meta-Statistiken (JSON, zur Laufzeit erzeugt) +``` + +**Architekturprinzipien:** +- **Daten von Logik getrennt** — Gegner, Waffen und Items liegen als JSON vor und lassen sich ohne Code-Änderungen erweitern oder neu balancieren. +- **Room-Graph statt linearer Szenen** — Hub-Bereiche (Mars UAC Base, Hell Server) bestehen aus vernetzten `Room`-Objekten statt fest verdrahteter Szenenfolgen. +- **Eine generische Kampf-Engine** — dieselbe `run_combat()`-Funktion bedient sowohl einfache Gegner als auch mehrphasige Bosse. +- **Seed-injizierbarer Zufall** — jede Funktion, die Zufallswerte braucht, nimmt ein `random.Random`-Objekt entgegen, was reproduzierbare Testläufe und den `--seed`-Modus ermöglicht. + +--- + +## Erweitern des Spiels + +- **Neue Gegner:** Eintrag in `doom_matrix/data/enemies.json` ergänzen (id, name, type, hp, stagger_max, attack_range, firewall, optional boss/phases). +- **Neue Waffen:** Eintrag in `doom_matrix/data/weapons.json` ergänzen und in `entities/player.py` bzw. der jeweiligen Szene über `player.add_weapon("")` vergeben. +- **Neue Räume/Hubs:** `world/rooms.py`s `Room`/`Hub`-Klassen verwenden; siehe `world/layers.py` (`mars_base`, `hell_server`) als Referenz. +- **Neue Enden:** `world/endings.py::determine_ending()` um eine weitere Bedingung erweitern und einen passenden `_ending_*`-Handler ergänzen. +- **Neue Cheat-Codes:** in `engine/cheats.py::try_cheat()` ergänzen. + +--- + +*Erstellt von LXC für Andy.* 🐇 diff --git a/doom-matrix-adventure.py b/doom-matrix-adventure.py new file mode 100644 index 0000000..2363b24 --- /dev/null +++ b/doom-matrix-adventure.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +DOOM × MATRIX: SYSTEM FAILURE +Startpunkt (Kompatibilitaets-Wrapper). Die eigentliche Implementierung +liegt jetzt im Paket doom_matrix/ (siehe doom_matrix/engine/, entities/, +systems/, world/, data/). +""" + +from doom_matrix.engine.game import main + +if __name__ == "__main__": + main() diff --git a/doom_matrix/__init__.py b/doom_matrix/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/doom_matrix/data/enemies.json b/doom_matrix/data/enemies.json new file mode 100644 index 0000000..c1ee153 --- /dev/null +++ b/doom_matrix/data/enemies.json @@ -0,0 +1,86 @@ +[ + { + "id": "agent_imp", + "name": "Agent Imp", + "type": "hybrid", + "hp": 50, + "stagger_max": 30, + "attack_range": [5, 15], + "firewall": 8, + "description": "Ein Scout-Agent mit dämonischen Zügen. Schuppen unter dem Anzug." + }, + { + "id": "imp", + "name": "Imp", + "type": "demon", + "hp": 35, + "stagger_max": 20, + "attack_range": [4, 10], + "firewall": 3, + "description": "Ein kleiner, hüpfender Dämon mit Klauen aus Klartext." + }, + { + "id": "pinky", + "name": "Pinky", + "type": "demon", + "hp": 60, + "stagger_max": 35, + "attack_range": [8, 16], + "firewall": 2, + "description": "Gepanzert von vorne. Nur von hinten wirklich verwundbar." + }, + { + "id": "cacodemon", + "name": "Cacodemon", + "type": "demon", + "hp": 70, + "stagger_max": 40, + "attack_range": [10, 20], + "firewall": 4, + "description": "Ein schwebender roter Ball aus Zähnen und Hass." + }, + { + "id": "revenant", + "name": "Revenant", + "type": "hybrid", + "hp": 65, + "stagger_max": 35, + "attack_range": [10, 22], + "firewall": 10, + "description": "Skelett mit Server-Raketenwerfern auf den Schultern." + }, + { + "id": "agent_clone", + "name": "Agent-Klon", + "type": "program", + "hp": 45, + "stagger_max": 28, + "attack_range": [8, 18], + "firewall": 15, + "description": "Eine Kopie von Smith. Perfekt repliziert. Perfekt ersetzbar." + }, + { + "id": "cyberdemon", + "name": "Cyberdemon", + "type": "hybrid", + "hp": 180, + "stagger_max": 80, + "attack_range": [15, 30], + "firewall": 20, + "description": "Halb Fleisch, halb Server-Rack. Raketenwerfer mit Ethernet-Kabeln.", + "boss": true, + "phases": 2 + }, + { + "id": "archi_lord", + "name": "Der Archi-Lord", + "type": "program", + "hp": 220, + "stagger_max": 100, + "attack_range": [12, 28], + "firewall": 30, + "description": "Halb Architekt, halb Dark Lord Davoth. Die Quelle von allem.", + "boss": true, + "phases": 3 + } +] diff --git a/doom_matrix/data/items.json b/doom_matrix/data/items.json new file mode 100644 index 0000000..b1d177a --- /dev/null +++ b/doom_matrix/data/items.json @@ -0,0 +1,82 @@ +[ + { + "id": "medkit", + "name": "Medkit", + "description": "Heilt 30 HP.", + "kind": "consumable", + "stackable": true, + "value": 30 + }, + { + "id": "armor_shard", + "name": "Armor-Shard", + "description": "Gibt 15 Armor.", + "kind": "consumable", + "stackable": true, + "value": 15 + }, + { + "id": "ammo_pack", + "name": "Ammo-Pack", + "description": "Füllt Munition aller Waffen auf.", + "kind": "consumable", + "stackable": true, + "value": 1 + }, + { + "id": "backup_pill", + "name": "Backup-Pille", + "description": "Einmaliges Auto-Revive. Ein Checkpoint in Pillenform.", + "kind": "consumable", + "stackable": false, + "value": 1 + }, + { + "id": "keycard_red", + "name": "Rote Keycard", + "description": "Öffnet rote Sicherheitstüren.", + "kind": "key", + "stackable": false, + "value": 0 + }, + { + "id": "keycard_blue", + "name": "Blaue Keycard", + "description": "Öffnet blaue Sicherheitstüren.", + "kind": "key", + "stackable": false, + "value": 0 + }, + { + "id": "keycard_yellow", + "name": "Gelbe Keycard", + "description": "Öffnet gelbe Sicherheitstüren.", + "kind": "key", + "stackable": false, + "value": 0 + }, + { + "id": "crucible_fragment", + "name": "Crucible-Fragment", + "description": "Ein Splitter reinen Codes. Sammle drei für die letzte Waffe.", + "kind": "fragment", + "stackable": true, + "value": 1 + }, + { + "id": "praetor_token", + "name": "Praetor-Token", + "description": "Ein Skillpunkt für Upgrades.", + "kind": "misc", + "stackable": true, + "value": 1 + }, + { + "id": "logfile", + "name": "Log-Datei", + "description": "Ein gefundenes Fragment vergangener Wahrheit.", + "kind": "lore", + "stackable": true, + "value": 0 + } +] diff --git a/doom_matrix/data/weapons.json b/doom_matrix/data/weapons.json new file mode 100644 index 0000000..0b97d8d --- /dev/null +++ b/doom_matrix/data/weapons.json @@ -0,0 +1,47 @@ +[ + { + "id": "fists", + "name": "Fäuste", + "damage_range": [3, 8], + "ammo_type": "melee", + "ammo_cost": 0, + "damage_type": "melee", + "description": "Nicht sehr effektiv, aber immer verfügbar." + }, + { + "id": "super_shotgun", + "name": "Super Shotgun", + "damage_range": [15, 25], + "ammo_type": "shells", + "ammo_cost": 2, + "damage_type": "ballistic", + "description": "Klassisch. Brutal. Doom in Reinform." + }, + { + "id": "plasma_rifle", + "name": "Plasma Rifle", + "damage_range": [10, 20], + "ammo_type": "cells", + "ammo_cost": 10, + "damage_type": "plasma", + "description": "Matrix-Style. Besonders wirksam gegen Programme." + }, + { + "id": "bfg10000", + "name": "BFG 10000", + "damage_range": [60, 90], + "ammo_type": "bfg", + "ammo_cost": 1, + "damage_type": "plasma", + "description": "'Big Fucking Gun'. Verbraucht enorme Energie." + }, + { + "id": "crucible", + "name": "Crucible Sword", + "damage_range": [30, 45], + "ammo_type": "melee", + "ammo_cost": 0, + "damage_type": "melee", + "description": "Ein Schwert aus reinem Code. Ignoriert Armor." + } +] diff --git a/doom_matrix/engine/__init__.py b/doom_matrix/engine/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/doom_matrix/engine/cheats.py b/doom_matrix/engine/cheats.py new file mode 100644 index 0000000..80ae95f --- /dev/null +++ b/doom_matrix/engine/cheats.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Cheat-Codes / Easter Eggs, die an mehreren Eingabepunkten abgefangen werden.""" + +from doom_matrix.engine.ui import Colors, slow_print, dramatic_pause + + +def try_cheat(raw_input, player): + """Prueft, ob raw_input ein Cheat-Code ist. Wendet ihn an und gibt True zurueck, + falls ja -- der Aufrufer soll die Eingabeaufforderung dann wiederholen.""" + code = raw_input.strip().lower() + + if code == "iddqd": + player.flags["god_mode"] = not player.flags.get("god_mode", False) + state = "AKTIVIERT" if player.flags["god_mode"] else "DEAKTIVIERT" + slow_print(f"\n{Colors.YELLOW}{Colors.BOLD}GOD MODE {state}.{Colors.END}") + _track(player, code) + return True + + if code == "idkfa": + for wid in ("super_shotgun", "plasma_rifle", "bfg10000"): + player.add_weapon(wid) + player.ammo["shells"] = 999 + player.ammo["cells"] = 999 + player.ammo["bfg"] = 99 + slow_print(f"\n{Colors.YELLOW}{Colors.BOLD}ALLE WAFFEN + MUNITION GEWAEHRT.{Colors.END}") + _track(player, code) + return True + + if code == "thereisnospoon": + slow_print(f"\n{Colors.GREEN}Du starrst auf deine Hand. Es gibt keinen Loeffel.{Colors.END}") + slow_print(f"{Colors.GREEN}Deine Corruption sinkt leicht -- Klarheit hat ihren Preis in Gewissheit.{Colors.END}") + player.corruption = max(0, player.corruption - 10) + _track(player, code) + return True + + if code == "xyzzy": + slow_print(f"\n{Colors.DIM}Ein hohles Echo antwortet. Nichts passiert. (Das war schon 1977 ein Scherz.){Colors.END}") + _track(player, code) + return True + + if code in ("doom", "knee-deep", "kneedeepinthedead"): + _knee_deep_easter_egg(player) + _track(player, code) + return True + + return False + + +def _track(player, code): + if code not in player.flags["cheats_used"]: + player.flags["cheats_used"].append(code) + + +def _knee_deep_easter_egg(player): + slow_print(f"\n{Colors.GREEN}{Colors.BOLD}═══ EASTER EGG: KNEE-DEEP IN THE DEAD ═══{Colors.END}") + slow_print(f"{Colors.WHITE}Fuer einen Moment wird die Welt zu reinem Pixel-Grau.{Colors.END}") + slow_print(f"{Colors.WHITE}Ein winziger, viereckiger Imp huepft auf dich zu -- 16x16 Pixel Nostalgie.{Colors.END}") + dramatic_pause(0.5) + slow_print(f"{Colors.YELLOW}Er kann dir nichts tun. Er ist nur eine Erinnerung.{Colors.END}") + slow_print(f"{Colors.YELLOW}Du zerquetschst ihn trotzdem. Fuer alte Zeiten.{Colors.END}") + slow_print(f"{Colors.GREEN}+1 Praetor-Token (Nostalgie-Bonus){Colors.END}") + player.praetor_tokens += 1 diff --git a/doom_matrix/engine/game.py b/doom_matrix/engine/game.py new file mode 100644 index 0000000..73e8d54 --- /dev/null +++ b/doom_matrix/engine/game.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Game-Loop, Titel-Bildschirm, Klassenwahl, Szenen-Dispatch.""" + +import random +import sys + +from doom_matrix.engine.ui import Colors, clear_screen, slow_print, dramatic_pause, menu, set_fast_mode +from doom_matrix.engine import save +from doom_matrix.entities.player import Player, CLASSES +from doom_matrix.world import layers, endings + + +def title_screen(): + clear_screen() + print(f""" +{Colors.RED}{Colors.BOLD} + ██████╗ ██████╗ ██████╗ ███╗ ███╗ + ██╔══██╗██╔═══██╗██╔═══██╗████╗ ████║ + ██║ ██║██║ ██║██║ ██║██╔████╔██║ + ██║ ██║██║ ██║██║ ██║██║╚██╔╝██║ + ██████╔╝╚██████╔╝╚██████╔╝██║ ╚═╝ ██║ + ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ +{Colors.END} +{Colors.GREEN}{Colors.BOLD} + ███╗ ███╗ █████╗ ████████╗██████╗ ██╗██╗ ██╗ + ████╗ ████║██╔══██╗╚══██╔══╝██╔══██╗██║╚██╗██╔╝ + ██╔████╔██║███████║ ██║ ██████╔╝██║ ╚███╔╝ + ██║╚██╔╝██║██╔══██║ ██║ ██╔══██╗██║ ██╔██╗ + ██║ ╚═╝ ██║██║ ██║ ██║ ██║ ██║██║██╔╝ ██╗ + ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ +{Colors.END} +{Colors.YELLOW}{Colors.BOLD} + ╔═══════════════════════════════════════╗ + ║ S Y S T E M F A I L U R E ║ + ╚═══════════════════════════════════════╝ +{Colors.END} +{Colors.DIM} + "What if the Matrix was just a very + inefficient DOOM engine?" +{Colors.END} + """) + print(f"\n{Colors.CYAN}[Drücke ENTER um zu starten]{Colors.END}") + input() + + +def choose_class(): + clear_screen() + print(f"\n{Colors.BOLD}{Colors.CYAN}═══ WÄHLE DEINE KLASSE ═══{Colors.END}\n") + keys = list(CLASSES.keys()) + for i, key in enumerate(keys): + data = CLASSES[key] + print(f" {Colors.GREEN}[{i + 1}]{Colors.END} {Colors.BOLD}{data['name']}{Colors.END} — {data['description']}") + print(f" HP:{data['hp']} Armor:{data['armor']} Focus:{data['focus']} " + f"Hacking:{data['hacking']} Strength:{data['strength']}") + choice = input(f"\n{Colors.CYAN}Klasse: {Colors.END}").strip() + try: + idx = int(choice) - 1 + if 0 <= idx < len(keys): + return keys[idx] + except ValueError: + pass + return keys[0] + + +SCENES = { + "construct_intro": layers.construct_intro, + "white_rabbit_moment": layers.white_rabbit_moment, + "agent_fight": layers.agent_fight, + "after_fight": layers.after_fight, + "mars_base": layers.mars_base, + "matrix_deep": layers.matrix_deep, + "hell_server": layers.hell_server, + "final_boss": layers.final_boss, + "layer_zero_check": layers.layer_zero_check, +} + +TERMINAL_SCENES = {"ending", "game_over"} + + +def run_game(seed=None, fast=False): + set_fast_mode(fast) + rng = random.Random(seed) if seed is not None else random.Random() + + run_count = save.register_new_run() + + title_screen() + + class_key = choose_class() + player = Player(class_key) + + twist_line = save.loop_twist_line(run_count) + + current_scene = "construct_intro" + while current_scene not in TERMINAL_SCENES: + if twist_line and current_scene == "construct_intro": + clear_screen() + slow_print(f"\n{Colors.DIM}{twist_line}{Colors.END}") + dramatic_pause(1) + func = SCENES.get(current_scene) + if func is None: + break + current_scene = func(player, rng) + + if current_scene == "ending": + ending_key = endings.ending(player) + save.register_ending(ending_key) + elif current_scene == "game_over": + result = endings.game_over(player) + save.register_ending(result) + + save.save_game(player, slot="autosave") + + print(f"\n{Colors.CYAN}[ENTER zum Beenden]{Colors.END}") + input() + + +def main(): + fast = "--fast" in sys.argv + seed = None + for arg in sys.argv: + if arg.startswith("--seed="): + try: + seed = int(arg.split("=", 1)[1]) + except ValueError: + seed = None + + try: + run_game(seed=seed, fast=fast) + except KeyboardInterrupt: + print(f"\n\n{Colors.YELLOW}Spiel unterbrochen. Bis zum nächsten Mal! 🐇{Colors.END}") + sys.exit(0) diff --git a/doom_matrix/engine/save.py b/doom_matrix/engine/save.py new file mode 100644 index 0000000..f7e7c8b --- /dev/null +++ b/doom_matrix/engine/save.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Speichersystem (JSON-Savegames) und persistente Meta-Statistiken fuer den +Loop-Twist: das Spiel 'erinnert' sich, wie oft es schon gespielt wurde.""" + +import json +import os + +from doom_matrix.entities.player import Player + +_SAVE_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "saves") +_META_PATH = os.path.join(_SAVE_DIR, "meta.json") + +_DEFAULT_META = {"run_count": 0, "completed_runs": 0, "endings_seen": []} + + +def _ensure_dir(): + os.makedirs(_SAVE_DIR, exist_ok=True) + + +def save_game(player, slot="autosave"): + _ensure_dir() + path = os.path.join(_SAVE_DIR, f"{slot}.json") + with open(path, "w", encoding="utf-8") as f: + json.dump(player.to_dict(), f, ensure_ascii=False, indent=2) + return path + + +def load_game(slot="autosave"): + path = os.path.join(_SAVE_DIR, f"{slot}.json") + if not os.path.exists(path): + return None + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + return Player.from_dict(data) + + +def save_exists(slot="autosave"): + return os.path.exists(os.path.join(_SAVE_DIR, f"{slot}.json")) + + +def load_meta(): + if not os.path.exists(_META_PATH): + return dict(_DEFAULT_META) + try: + with open(_META_PATH, "r", encoding="utf-8") as f: + data = json.load(f) + merged = dict(_DEFAULT_META) + merged.update(data) + return merged + except (json.JSONDecodeError, OSError): + return dict(_DEFAULT_META) + + +def save_meta(meta): + _ensure_dir() + with open(_META_PATH, "w", encoding="utf-8") as f: + json.dump(meta, f, ensure_ascii=False, indent=2) + + +def register_new_run(): + meta = load_meta() + meta["run_count"] += 1 + save_meta(meta) + return meta["run_count"] + + +def register_ending(ending_key): + meta = load_meta() + meta["completed_runs"] += 1 + if ending_key not in meta["endings_seen"]: + meta["endings_seen"].append(ending_key) + save_meta(meta) + return meta + + +def loop_twist_line(run_count): + """Gibt einen Dialogzeile zurueck, falls das Spiel schon oefter gelaufen ist -- oder None.""" + if run_count <= 1: + return None + if run_count == 2: + return "Morpheus Hayden: 'Habe ich das hier nicht schon einmal gesagt?'" + if run_count < 5: + return f"Morpheus Hayden: 'Lauf Nummer {run_count}. Du wirst besser. Oder ich erinnere mich besser.'" + return f"Morpheus Hayden: 'Lauf Nummer {run_count}. Ehrlich gesagt -- ich weiss nicht mehr, welcher von uns beiden real ist.'" diff --git a/doom_matrix/engine/ui.py b/doom_matrix/engine/ui.py new file mode 100644 index 0000000..9abb5fa --- /dev/null +++ b/doom_matrix/engine/ui.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""UI-Hilfsfunktionen: Farben, Textausgabe, Balken, Glitch-Effekte.""" + +import os +import sys +import time +import random + +try: + sys.stdout.reconfigure(encoding='utf-8', errors='replace') +except (AttributeError, ValueError): + pass + +try: + import colorama + colorama.init() +except ImportError: + pass + +if os.name == 'nt': + os.system('') # aktiviert ANSI-Codes im klassischen Windows-Terminal + + +class Colors: + GREEN = '\033[92m' + RED = '\033[91m' + CYAN = '\033[96m' + YELLOW = '\033[93m' + MAGENTA = '\033[95m' + WHITE = '\033[97m' + BLACK = '\033[30m' + BOLD = '\033[1m' + DIM = '\033[2m' + BLINK = '\033[5m' + END = '\033[0m' + + +# Globaler Schalter für --fast Flag; wenn True, werden alle Delays uebersprungen +FAST_MODE = False + + +def set_fast_mode(enabled): + global FAST_MODE + FAST_MODE = enabled + + +def slow_print(text, delay=0.02): + """Langsames Tippen fuer Dramatik (uebersprungen im --fast Modus).""" + if FAST_MODE: + print(text) + return + for char in text: + print(char, end='', flush=True) + time.sleep(delay) + print() + + +def dramatic_pause(seconds=1): + if FAST_MODE: + return + time.sleep(seconds) + + +def clear_screen(): + os.system('cls' if os.name == 'nt' else 'clear') + + +def bar(val, max_val, color, width=20): + val = max(0, min(val, max_val)) + filled = int(val / max_val * width) if max_val else 0 + filled = max(0, min(width, filled)) + b = '█' * filled + '░' * (width - filled) + return f"{color}{b}{Colors.END} {val}/{max_val}" + + +def glitch_text(text, intensity=0.15, rng=None): + """Verwuerfelt zufaellig Zeichen in einem String -- fuer Deja-vu-/Bug-Momente.""" + rng = rng or random + glitch_chars = "#%&@$!?/\\|01" + out = [] + for ch in text: + if ch != ' ' and rng.random() < intensity: + out.append(rng.choice(glitch_chars)) + else: + out.append(ch) + return ''.join(out) + + +def print_glitch_sequence(text, cycles=3, delay=0.04, color=Colors.GREEN): + """Zeigt kurz Glitch-Varianten eines Textes an, bevor der echte Text steht.""" + if FAST_MODE: + print(f"{color}{text}{Colors.END}") + return + for _ in range(cycles): + print(f"\r{color}{glitch_text(text, 0.4)}{Colors.END}", end='', flush=True) + time.sleep(delay) + print(f"\r{color}{text}{Colors.END}") + + +def divider(char='═', width=45, color=Colors.CYAN): + print(f"{color}{char * width}{Colors.END}") + + +def section_title(title, color=Colors.CYAN): + print(f"\n{Colors.BOLD}{color}═══ {title} ═══{Colors.END}") + + +def menu(options, prompt="Aktion"): + """options: Liste von (key, label) Tupeln. Gibt die gewaehlte Eingabe zurueck.""" + for key, label in options: + print(f" {Colors.GREEN}[{key}]{Colors.END} {label}") + return input(f"\n{Colors.CYAN}{prompt}: {Colors.END}").strip() diff --git a/doom_matrix/entities/__init__.py b/doom_matrix/entities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/doom_matrix/entities/enemy.py b/doom_matrix/entities/enemy.py new file mode 100644 index 0000000..bf105b2 --- /dev/null +++ b/doom_matrix/entities/enemy.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Enemy-Klasse: datengetriebene Gegner mit Stagger-System.""" + +import json +import os + +_DATA_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "enemies.json") +_ENEMY_TEMPLATES = None + + +def _load_templates(): + global _ENEMY_TEMPLATES + if _ENEMY_TEMPLATES is None: + with open(_DATA_PATH, "r", encoding="utf-8") as f: + data = json.load(f) + _ENEMY_TEMPLATES = {d["id"]: d for d in data} + return _ENEMY_TEMPLATES + + +class Enemy: + def __init__(self, template): + self.enemy_id = template["id"] + self.name = template["name"] + self.type = template["type"] # demon, program, hybrid + self.max_hp = template["hp"] + self.hp = template["hp"] + self.stagger_max = template["stagger_max"] + self.stagger = 0 + self.attack_range = tuple(template["attack_range"]) + self.firewall = template.get("firewall", 5) + self.description = template.get("description", "") + self.is_boss = template.get("boss", False) + self.max_phases = template.get("phases", 1) + self.phase = 1 + self.staggered = False + + @classmethod + def create(cls, enemy_id): + templates = _load_templates() + return cls(templates[enemy_id]) + + @property + def is_alive(self): + return self.hp > 0 + + def apply_damage(self, amount): + self.hp = max(0, self.hp - amount) + if not self.staggered: + self.stagger = min(self.stagger_max, self.stagger + amount) + if self.stagger >= self.stagger_max: + self.staggered = True + + def clear_stagger(self): + self.staggered = False + self.stagger = 0 + + def maybe_advance_phase(self): + """Bosse verlieren pro Phase einen Teil ihres HP-Schwellenwerts und werden staerker.""" + if not self.is_boss or self.phase >= self.max_phases: + return False + threshold = self.max_hp * (1 - self.phase / self.max_phases) + if self.hp <= threshold and self.hp > 0: + self.phase += 1 + self.attack_range = (self.attack_range[0] + 4, self.attack_range[1] + 6) + return True + return False diff --git a/doom_matrix/entities/items.py b/doom_matrix/entities/items.py new file mode 100644 index 0000000..e12085f --- /dev/null +++ b/doom_matrix/entities/items.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Item-, Waffen- und Inventar-Klassen.""" + + +class Item: + def __init__(self, item_id, name, description, kind="misc", stackable=True, value=1): + self.item_id = item_id + self.name = name + self.description = description + self.kind = kind # misc, consumable, key, lore, fragment + self.stackable = stackable + self.value = value # z.B. Heilmenge bei consumables + + @classmethod + def from_dict(cls, d): + return cls( + item_id=d["id"], + name=d["name"], + description=d.get("description", ""), + kind=d.get("kind", "misc"), + stackable=d.get("stackable", True), + value=d.get("value", 1), + ) + + +class Weapon: + def __init__(self, weapon_id, name, damage_range, ammo_type, ammo_cost, + damage_type="ballistic", description=""): + self.weapon_id = weapon_id + self.name = name + self.damage_range = tuple(damage_range) # (min, max) + self.ammo_type = ammo_type # "melee" fuer unbegrenzt + self.ammo_cost = ammo_cost + self.damage_type = damage_type # ballistic, plasma, melee + self.description = description + + @classmethod + def from_dict(cls, d): + return cls( + weapon_id=d["id"], + name=d["name"], + damage_range=d["damage_range"], + ammo_type=d.get("ammo_type", "melee"), + ammo_cost=d.get("ammo_cost", 0), + damage_type=d.get("damage_type", "ballistic"), + description=d.get("description", ""), + ) + + def __repr__(self): + return self.name + + +# Waffen-Dreieck: Schaden-Multiplikator (Waffentyp -> Gegnertyp) +TYPE_ADVANTAGE = { + ("ballistic", "demon"): 1.3, + ("ballistic", "program"): 0.8, + ("ballistic", "hybrid"): 1.0, + ("plasma", "program"): 1.3, + ("plasma", "hybrid"): 1.15, + ("plasma", "demon"): 0.85, + ("melee", "demon"): 1.0, + ("melee", "program"): 1.0, + ("melee", "hybrid"): 1.1, +} + + +def type_multiplier(damage_type, enemy_type): + return TYPE_ADVANTAGE.get((damage_type, enemy_type), 1.0) + + +class Inventory: + def __init__(self): + self.items = {} # item_id -> (Item, count) + self.keycards = set() # "red", "blue", "yellow" + self.crucible_fragments = 0 + self.has_backup_pill = False + + def add(self, item, count=1): + if item.kind == "key": + self.keycards.add(item.item_id) + return + if item.kind == "fragment": + self.crucible_fragments += count + return + if item.item_id == "backup_pill": + self.has_backup_pill = True + return + if item.item_id in self.items: + existing_item, existing_count = self.items[item.item_id] + self.items[item.item_id] = (existing_item, existing_count + count) + else: + self.items[item.item_id] = (item, count) + + def has(self, item_id): + return item_id in self.items and self.items[item_id][1] > 0 + + def consume(self, item_id): + if not self.has(item_id): + return None + item, count = self.items[item_id] + count -= 1 + if count <= 0: + del self.items[item_id] + else: + self.items[item_id] = (item, count) + return item + + def has_keycard(self, color): + return color in self.keycards + + def list_display(self): + lines = [] + for item, count in self.items.values(): + lines.append(f"{item.name} x{count}") + if self.keycards: + lines.append("Keycards: " + ", ".join(sorted(self.keycards))) + if self.crucible_fragments: + lines.append(f"Crucible-Fragmente: {self.crucible_fragments}/3") + if self.has_backup_pill: + lines.append("Backup-Pille (Auto-Revive bereit)") + return lines + + def is_empty(self): + return not self.items and not self.keycards and not self.crucible_fragments and not self.has_backup_pill diff --git a/doom_matrix/entities/player.py b/doom_matrix/entities/player.py new file mode 100644 index 0000000..b159eea --- /dev/null +++ b/doom_matrix/entities/player.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Player-Klasse: Stats, Klassen, Leveling, Inventar, Story-Flags.""" + +import json +import os + +from doom_matrix.engine.ui import Colors, bar +from doom_matrix.entities.items import Inventory, Weapon + +_WEAPON_DATA_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "weapons.json") +_WEAPON_TEMPLATES = None + + +def _load_weapon_templates(): + global _WEAPON_TEMPLATES + if _WEAPON_TEMPLATES is None: + with open(_WEAPON_DATA_PATH, "r", encoding="utf-8") as f: + data = json.load(f) + _WEAPON_TEMPLATES = {d["id"]: d for d in data} + return _WEAPON_TEMPLATES + + +def make_weapon(weapon_id): + templates = _load_weapon_templates() + return Weapon.from_dict(templates[weapon_id]) + + +# Charakterklassen: Basiswerte +CLASSES = { + "slayer": { + "name": "Slayer", + "description": "Doom-Fokus. Mehr HP und Staerke, schwaecheres Hacking.", + "hp": 120, "armor": 20, "focus": 60, "hacking": 2, "strength": 8, "corruption_rate": 1.0, + "start_weapon": "super_shotgun", "start_ammo": {"shells": 20}, + }, + "operator": { + "name": "Operator", + "description": "Matrix-Fokus. Starkes Hacking und Focus, koerperlich schwaecher.", + "hp": 85, "armor": 10, "focus": 100, "hacking": 9, "strength": 3, "corruption_rate": 1.0, + "start_weapon": "plasma_rifle", "start_ammo": {"cells": 100}, + }, + "hybrid": { + "name": "Anomalie", + "description": "Ausgewogen. Einzigartige Dialogoptionen, aber doppelte Corruption.", + "hp": 100, "armor": 15, "focus": 80, "hacking": 5, "strength": 5, "corruption_rate": 2.0, + "start_weapon": "super_shotgun", "start_ammo": {"shells": 10, "cells": 40}, + }, +} + + +class Player: + def __init__(self, player_class="slayer"): + cls_data = CLASSES[player_class] + self.player_class = player_class + self.class_name = cls_data["name"] + self.name = "The One Slayer" + + self.max_health = cls_data["hp"] + self.health = cls_data["hp"] + self.armor = cls_data["armor"] + self.max_focus = cls_data["focus"] + self.focus = cls_data["focus"] + self.hacking = cls_data["hacking"] + self.strength = cls_data["strength"] + self.corruption_rate = cls_data["corruption_rate"] + + self.corruption = 0 # 0-100 + self.level = 1 + self.praetor_tokens = 0 + + self.weapons = [make_weapon("fists")] + start_weapon = make_weapon(cls_data["start_weapon"]) + self.weapons.append(start_weapon) + self.ammo = {"bullets": 0, "shells": 0, "cells": 0, "bfg": 0} + self.ammo.update(cls_data["start_ammo"]) + + self.inventory = Inventory() + self.layer = 3 # Start in Matrix + self.location = "construct" + + self.glory_kills = 0 + self.hacks = 0 + self.trace_marks = 0 + + # Story-Flags fuer Easter Eggs / Verzweigungen + self.flags = { + "met_oracle": False, + "saw_rabbit": False, + "cat_glitch_count": 0, + "cat_glitch_seen": False, + "god_mode": False, + "cheats_used": [], + } + + def has_weapon(self, weapon_id): + return any(w.weapon_id == weapon_id for w in self.weapons) + + def get_weapon(self, weapon_id): + for w in self.weapons: + if w.weapon_id == weapon_id: + return w + return None + + def add_weapon(self, weapon_id): + if not self.has_weapon(weapon_id): + self.weapons.append(make_weapon(weapon_id)) + + def heal(self, amount): + self.health = min(self.max_health, self.health + amount) + + def take_damage(self, amount): + """Wendet Schaden an, Armor absorbiert zuerst. Gibt (final_damage, armor_absorbed) zurueck.""" + if self.flags.get("god_mode"): + return 0, 0 + armor_absorb = min(self.armor, amount) + self.armor -= armor_absorb + remaining = amount - armor_absorb + self.health = max(0, self.health - remaining) + return remaining, armor_absorb + + def add_armor(self, amount): + self.armor = min(100, self.armor + amount) + + def gain_corruption(self, amount): + self.corruption = min(100, self.corruption + amount * self.corruption_rate) + + def is_dead(self): + return self.health <= 0 + + def try_auto_revive(self): + if self.inventory.has_backup_pill and self.is_dead(): + self.inventory.has_backup_pill = False + self.health = int(self.max_health * 0.5) + return True + return False + + def spend_praetor_token(self, stat): + if self.praetor_tokens <= 0: + return False + self.praetor_tokens -= 1 + if stat == "health": + self.max_health += 15 + self.health = min(self.max_health, self.health + 15) + elif stat == "focus": + self.max_focus += 15 + self.focus = min(self.max_focus, self.focus + 15) + elif stat == "hacking": + self.hacking += 2 + elif stat == "strength": + self.strength += 2 + return True + + def _layer_name(self): + layers = { + 4: f"{Colors.RED}MARS UAC BASE{Colors.END}", + 3: f"{Colors.GREEN}THE MATRIX{Colors.END}", + 2: f"{Colors.MAGENTA}HELL SERVER{Colors.END}", + 1: f"{Colors.YELLOW}THE ARCHITECT'S CORE{Colors.END}", + 0: f"{Colors.WHITE}{Colors.BOLD}LAYER 0{Colors.END}", + } + return layers.get(self.layer, "UNKNOWN") + + def show_status(self): + print(f"\n{Colors.BOLD}{Colors.CYAN}═══ STATUS: {self.class_name} ═══{Colors.END}") + print(f"Health: {bar(self.health, self.max_health, Colors.RED)}") + print(f"Armor: {bar(self.armor, 100, Colors.YELLOW)}") + print(f"Focus: {bar(self.focus, self.max_focus, Colors.GREEN)}") + print(f"Corruption: {bar(int(self.corruption), 100, Colors.MAGENTA)}") + print(f"Hacking: {self.hacking} Strength: {self.strength} Level: {self.level}") + print(f"Layer: {self._layer_name()}") + print(f"Weapons: {', '.join(w.name for w in self.weapons)}") + if any(self.ammo.values()): + print(f"Ammo: {', '.join([f'{k}={v}' for k, v in self.ammo.items() if v > 0])}") + inv_lines = self.inventory.list_display() + if inv_lines: + print(f"Inventar: {' | '.join(inv_lines)}") + if self.praetor_tokens: + print(f"{Colors.YELLOW}Praetor-Token verfuegbar: {self.praetor_tokens}{Colors.END}") + print(f"{Colors.CYAN}══════════════════════════════{Colors.END}\n") + + def to_dict(self): + return { + "player_class": self.player_class, + "health": self.health, + "max_health": self.max_health, + "armor": self.armor, + "focus": self.focus, + "max_focus": self.max_focus, + "hacking": self.hacking, + "strength": self.strength, + "corruption": self.corruption, + "level": self.level, + "praetor_tokens": self.praetor_tokens, + "weapon_ids": [w.weapon_id for w in self.weapons], + "ammo": self.ammo, + "layer": self.layer, + "location": self.location, + "glory_kills": self.glory_kills, + "hacks": self.hacks, + "trace_marks": self.trace_marks, + "flags": self.flags, + "keycards": list(self.inventory.keycards), + "crucible_fragments": self.inventory.crucible_fragments, + "has_backup_pill": self.inventory.has_backup_pill, + } + + @classmethod + def from_dict(cls, d): + p = cls(d.get("player_class", "slayer")) + p.health = d["health"] + p.max_health = d["max_health"] + p.armor = d["armor"] + p.focus = d["focus"] + p.max_focus = d["max_focus"] + p.hacking = d["hacking"] + p.strength = d["strength"] + p.corruption = d["corruption"] + p.level = d["level"] + p.praetor_tokens = d["praetor_tokens"] + p.weapons = [make_weapon(wid) for wid in d["weapon_ids"]] + p.ammo = d["ammo"] + p.layer = d["layer"] + p.location = d["location"] + p.glory_kills = d["glory_kills"] + p.hacks = d["hacks"] + p.trace_marks = d["trace_marks"] + p.flags = d["flags"] + p.inventory.keycards = set(d.get("keycards", [])) + p.inventory.crucible_fragments = d.get("crucible_fragments", 0) + p.inventory.has_backup_pill = d.get("has_backup_pill", False) + return p diff --git a/doom_matrix/main.py b/doom_matrix/main.py new file mode 100644 index 0000000..39fa4f5 --- /dev/null +++ b/doom_matrix/main.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +DOOM × MATRIX: SYSTEM FAILURE +Ein interaktives Text-Adventure -- Einstiegspunkt. +""" + +import os +import sys + +if __package__ in (None, ""): + sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from doom_matrix.engine.game import main + +if __name__ == "__main__": + main() diff --git a/doom_matrix/systems/__init__.py b/doom_matrix/systems/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/doom_matrix/systems/combat.py b/doom_matrix/systems/combat.py new file mode 100644 index 0000000..b39185f --- /dev/null +++ b/doom_matrix/systems/combat.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Generische Kampf-Engine: Stagger, Bullet Time (Focus), Glory Kills, Hacking.""" + +import random + +from doom_matrix.engine.ui import Colors, slow_print, dramatic_pause, bar, menu +from doom_matrix.engine import cheats +from doom_matrix.entities.items import type_multiplier +from doom_matrix.entities.enemy import Enemy +from doom_matrix.systems import hacking + + +GLORY_HEAL = 25 +FOCUS_COST_BULLET_TIME = 25 +FOCUS_REGEN_PER_TURN = 8 + + +def _init_status(enemy): + if not hasattr(enemy, "frozen"): + enemy.frozen = False + if not hasattr(enemy, "jammed"): + enemy.jammed = 0 + if not hasattr(enemy, "turned"): + enemy.turned = False + + +def _pick_target(enemies): + alive = [e for e in enemies if e.is_alive] + if len(alive) == 1: + return alive[0] + print(f"\n{Colors.RED}Ziele:{Colors.END}") + for i, e in enumerate(alive): + stagger_tag = f" {Colors.YELLOW}[STAGGERED]{Colors.END}" if e.staggered else "" + print(f" {Colors.GREEN}[{i + 1}]{Colors.END} {e.name} - {bar(e.hp, e.max_hp, Colors.RED)}{stagger_tag}") + choice = input(f"\n{Colors.CYAN}Ziel waehlen: {Colors.END}").strip() + try: + idx = int(choice) - 1 + if 0 <= idx < len(alive): + return alive[idx] + except ValueError: + pass + return alive[0] + + +def _weapon_menu(player): + print(f"\n{Colors.GREEN}Waffen:{Colors.END}") + usable = [] + for w in player.weapons: + if w.ammo_type == "melee": + usable.append(w) + tag = "unbegrenzt" + else: + have = player.ammo.get(w.ammo_type, 0) + tag = f"{have} {w.ammo_type}" + if have >= w.ammo_cost: + usable.append(w) + print(f" {Colors.GREEN}[{len(usable) if w in usable else '-'}]{Colors.END} {w.name} ({tag})") + options = [(str(i + 1), w.name) for i, w in enumerate(usable)] + choice = menu(options, "Waffe") + try: + idx = int(choice) - 1 + if 0 <= idx < len(usable): + return usable[idx] + except ValueError: + pass + return usable[0] if usable else player.get_weapon("fists") + + +def _fire_weapon(player, weapon, enemy, rng): + dmg = rng.randint(*weapon.damage_range) + dmg = int(dmg * type_multiplier(weapon.damage_type, enemy.type)) + if weapon.ammo_type != "melee": + player.ammo[weapon.ammo_type] = max(0, player.ammo[weapon.ammo_type] - weapon.ammo_cost) + return dmg + + +def _enemy_attack(player, enemy, rng): + if enemy.frozen: + slow_print(f"\n{Colors.GREEN}{enemy.name} ist eingefroren -- kein Angriff diese Runde!{Colors.END}") + enemy.frozen = False + return + if enemy.turned: + slow_print(f"\n{Colors.GREEN}{enemy.name} greift, verwirrt, ins Leere.{Colors.END}") + enemy.turned = False + return + + dmg = rng.randint(*enemy.attack_range) + if enemy.jammed: + dmg = int(dmg * 0.5) + enemy.jammed -= 1 + + remaining, absorbed = player.take_damage(dmg) + if absorbed: + slow_print(f"\n{Colors.RED}{enemy.name} greift an! Armor absorbiert {absorbed} Schaden.{Colors.END}") + if remaining: + slow_print(f"{Colors.RED}Du erleidest {remaining} Schaden!{Colors.END}") + if not absorbed and not remaining: + slow_print(f"\n{Colors.GREEN}{enemy.name} greift an -- God Mode absorbiert alles!{Colors.END}") + + +def run_combat(player, enemies, title="KAMPF", rng=None, on_reinforcement=None): + """ + enemies: Liste von Enemy-Objekten. + Gibt True (Sieg) oder False (Tod, sofern kein Auto-Revive) zurueck. + """ + rng = rng or random + for e in enemies: + _init_status(e) + + slow_print(f"\n{Colors.BOLD}{Colors.RED}═══ {title} ═══{Colors.END}") + + while any(e.is_alive for e in enemies) and player.health > 0: + print() + for e in enemies: + if e.is_alive: + stagger_tag = f" {Colors.YELLOW}[STAGGERED - Glory Kill bereit!]{Colors.END}" if e.staggered else "" + print(f"{Colors.RED}{e.name}:{Colors.END} {bar(e.hp, e.max_hp, Colors.RED)}" + f" | Stagger {bar(e.stagger, e.stagger_max, Colors.YELLOW, 10)}{stagger_tag}") + print(f"{Colors.CYAN}Du:{Colors.END} {bar(player.health, player.max_health, Colors.RED)} | " + f"Armor {player.armor} | Focus {bar(player.focus, player.max_focus, Colors.GREEN, 10)}") + + options = [ + ("1", "Schiessen"), + ("2", f"Bullet Time (-{FOCUS_COST_BULLET_TIME} Focus, garantiertes Ausweichen + Krit)"), + ("3", "Glory Kill (nur bei gestaggertem Gegner)"), + ("4", f"Hack (Skill-Check, Hacking {player.hacking})"), + ] + if player.inventory.has("medkit"): + options.append(("5", "Medkit benutzen")) + + action = menu(options, "Aktion") + if cheats.try_cheat(action, player): + continue + bullet_time_active = False + + if action == "1": + weapon = _weapon_menu(player) + target = _pick_target(enemies) + dmg = _fire_weapon(player, weapon, target, rng) + slow_print(f"\n{Colors.YELLOW}{weapon.name} trifft {target.name}!{Colors.END}") + target.apply_damage(dmg) + slow_print(f"{Colors.RED}-{dmg} HP für {target.name}!{Colors.END}") + if target.staggered: + slow_print(f"{Colors.YELLOW}{Colors.BOLD}{target.name} ist GESTAGGERT! Glory Kill moeglich!{Colors.END}") + + elif action == "2": + if player.focus >= FOCUS_COST_BULLET_TIME: + player.focus -= FOCUS_COST_BULLET_TIME + bullet_time_active = True + slow_print(f"\n{Colors.GREEN}Zeit verlangsamt sich. Grüner Code umhüllt dich.{Colors.END}") + weapon = _weapon_menu(player) + target = _pick_target(enemies) + dmg = int(_fire_weapon(player, weapon, target, rng) * 1.5) + slow_print(f"{Colors.YELLOW}Kritischer Treffer in Bullet Time! {weapon.name} zerreisst {target.name}!{Colors.END}") + target.apply_damage(dmg) + slow_print(f"{Colors.RED}-{dmg} HP für {target.name}!{Colors.END}") + else: + slow_print(f"\n{Colors.RED}Nicht genug Focus fuer Bullet Time! Der Versuch kostet dich die Deckung.{Colors.END}") + + elif action == "3": + target = next((e for e in enemies if e.is_alive and e.staggered), None) + if target: + slow_print(f"\n{Colors.RED}{Colors.BOLD}GLORY KILL!{Colors.END}") + slow_print(f"{Colors.RED}Du stürmst vor, packst {target.name} und reisst ihn auseinander!{Colors.END}") + slow_print(f"{Colors.RED}Grüne Flüssigkeit spritzt — kein Blut. Code.{Colors.END}") + target.hp = 0 + player.heal(GLORY_HEAL) + player.glory_kills += 1 + slow_print(f"{Colors.GREEN}+{GLORY_HEAL} HP! Der Tod heilt dich.{Colors.END}") + else: + slow_print(f"\n{Colors.YELLOW}Kein Gegner ist gestaggert! Der Glory Kill schlaegt fehl.{Colors.END}") + target = _pick_target(enemies) + dmg = rng.randint(5, 10) + player.take_damage(dmg) + slow_print(f"{Colors.RED}{target.name} kontert! -{dmg} HP!{Colors.END}") + + elif action == "4": + target = _pick_target(enemies) + success = hacking.attempt_hack(player, target.firewall, rng) + if success: + effect = hacking.choose_hack_effect() + hacking.apply_hack_effect(effect, target, player, rng) + else: + slow_print(f"{Colors.RED}Das System hat deinen Hack erkannt! Firewall-Rueckstoss!{Colors.END}") + dmg = rng.randint(10, 20) + player.take_damage(dmg) + slow_print(f"{Colors.RED}-{dmg} HP durch Rueckstoss!{Colors.END}") + if hacking.trace_triggered(player): + slow_print(f"\n{Colors.MAGENTA}{Colors.BOLD}TRACE KOMPLETT! Das System schickt Verstaerkung!{Colors.END}") + if on_reinforcement: + reinforcement = on_reinforcement() + if reinforcement: + _init_status(reinforcement) + enemies.append(reinforcement) + slow_print(f"{Colors.RED}{reinforcement.name} betritt den Kampf!{Colors.END}") + + elif action == "5" and player.inventory.has("medkit"): + item = player.inventory.consume("medkit") + player.heal(item.value) + slow_print(f"\n{Colors.GREEN}Medkit benutzt. +{item.value} HP!{Colors.END}") + + else: + slow_print(f"{Colors.YELLOW}Ungueltige Eingabe.{Colors.END}") + continue + + # Boss-Phasenwechsel pruefen + for e in enemies: + if e.is_alive and e.maybe_advance_phase(): + slow_print(f"\n{Colors.MAGENTA}{Colors.BOLD}{e.name} tritt in Phase {e.phase} ein! Er wird gefaehrlicher!{Colors.END}") + e.clear_stagger() + + # Fokus regeneriert leicht jede Runde + player.focus = min(player.max_focus, player.focus + FOCUS_REGEN_PER_TURN) + + # Gegner greifen an (nicht wenn Bullet Time garantiertes Ausweichen gibt) + for e in enemies: + if e.is_alive and player.health > 0: + if bullet_time_active: + slow_print(f"\n{Colors.GREEN}Du weichst {e.name}s Angriff muehelos aus — Bullet Time!{Colors.END}") + continue + dramatic_pause(0.3) + _enemy_attack(player, e, rng) + + if player.health <= 0: + if player.try_auto_revive(): + slow_print(f"\n{Colors.MAGENTA}{Colors.BOLD}Die Backup-Pille aktiviert sich! Du wirst neu geladen...{Colors.END}") + slow_print(f"{Colors.GREEN}HP wiederhergestellt auf {player.health}.{Colors.END}") + else: + return False + + slow_print(f"\n{Colors.GREEN}{Colors.BOLD}ALLE GEGNER ELIMINIERT!{Colors.END}") + return True diff --git a/doom_matrix/systems/hacking.py b/doom_matrix/systems/hacking.py new file mode 100644 index 0000000..c29c656 --- /dev/null +++ b/doom_matrix/systems/hacking.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Hacking-Skill-Check-System: d20 + Hacking vs. Firewall, mit Trace-Marken.""" + +import random + +from doom_matrix.engine.ui import Colors, slow_print, menu + + +HACK_EFFECTS = [ + ("freeze", "Gegner einfrieren (verliert naechsten Zug)"), + ("jam", "Waffe des Gegners blockieren (-50% Schaden fuer 2 Runden)"), + ("leak", "HP-Leak (sofortiger Schaden ueber Zeit)"), + ("turn", "Gegner gegen Verbuendete aufhetzen (falls mehrere anwesend)"), +] + + +def attempt_hack(player, firewall, rng=None): + """Fuehrt einen Skill-Check durch. Gibt (erfolg, roll_text) zurueck.""" + rng = rng or random + roll = rng.randint(1, 20) + total = roll + player.hacking + success = total >= firewall + + slow_print(f"\n{Colors.GREEN}[HACK-CHECK] Wurf: {roll} + Hacking {player.hacking} = {total} " + f"vs. Firewall {firewall}{Colors.END}") + + if success: + player.hacks += 1 + else: + player.trace_marks += 1 + slow_print(f"{Colors.RED}Trace-Marke registriert! ({player.trace_marks}/3){Colors.END}") + + return success + + +def choose_hack_effect(): + print(f"\n{Colors.GREEN}Waehle deinen Hack-Effekt:{Colors.END}") + options = [(str(i + 1), label) for i, (_, label) in enumerate(HACK_EFFECTS)] + choice = menu(options, "Effekt") + try: + idx = int(choice) - 1 + if 0 <= idx < len(HACK_EFFECTS): + return HACK_EFFECTS[idx][0] + except ValueError: + pass + return HACK_EFFECTS[0][0] + + +def apply_hack_effect(effect, enemy, player, rng=None): + rng = rng or random + if effect == "freeze": + enemy.frozen = True + slow_print(f"{Colors.GREEN}{enemy.name} friert ein — System-Deadlock im naechsten Zug!{Colors.END}") + elif effect == "jam": + enemy.jammed = 2 + slow_print(f"{Colors.GREEN}Die Waffe von {enemy.name} blockiert. Schaden -50% fuer 2 Runden.{Colors.END}") + elif effect == "leak": + leak_dmg = rng.randint(10, 20) + enemy.apply_damage(leak_dmg) + slow_print(f"{Colors.RED}HP-Leak! {enemy.name} verliert {leak_dmg} HP ueber einen Pufferueberlauf.{Colors.END}") + elif effect == "turn": + slow_print(f"{Colors.GREEN}{enemy.name} wird kurzzeitig gegen die eigene Seite aufgehetzt.{Colors.END}") + enemy.turned = True + + +def trace_triggered(player): + """Wird True, wenn 3 Trace-Marken erreicht sind -- spawnt Verstaerkung.""" + if player.trace_marks >= 3: + player.trace_marks = 0 + return True + return False diff --git a/doom_matrix/world/__init__.py b/doom_matrix/world/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/doom_matrix/world/endings.py b/doom_matrix/world/endings.py new file mode 100644 index 0000000..06deb07 --- /dev/null +++ b/doom_matrix/world/endings.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Mehrere Enden je nach Corruption, Entscheidungen und Layer-0-Status.""" + +from doom_matrix.engine.ui import Colors, slow_print, dramatic_pause, clear_screen, menu +from doom_matrix.world.layers import layer_zero_unlocked + + +def determine_ending(player): + if layer_zero_unlocked(player) and player.flags.get("reached_layer_zero"): + return "wake_up" + if player.corruption >= 60: + return "dark_pact" + if player.flags.get("final_choice") == "3": + return "new_architect" + if player.flags.get("final_choice") == "4": + return "new_game_plus" + return "rip_and_tear" + + +def _stats_block(player): + slow_print(f"\n{Colors.CYAN}Statistiken:{Colors.END}") + print(f" Health übrig: {player.health}/{player.max_health}") + print(f" Corruption: {int(player.corruption)}%") + print(f" Glory Kills: {player.glory_kills}") + print(f" Hacks: {player.hacks}") + print(f" Klasse: {player.class_name}") + print(f" Waffen: {', '.join(w.name for w in player.weapons)}") + print(f" Crucible-Fragmente: {player.inventory.crucible_fragments}/3") + if player.flags.get("cheats_used"): + print(f" Benutzte Cheats: {', '.join(player.flags['cheats_used'])}") + + +def _banner(text, color=Colors.GREEN): + width = max(len(text) + 8, 55) + print(f""" +{color}{Colors.BOLD} + ╔{'═' * (width - 4)}╗ + ║{text.center(width - 4)}║ + ╚{'═' * (width - 4)}╝ +{Colors.END} + """) + + +def ending(player): + dramatic_pause(1) + clear_screen() + + kind = determine_ending(player) + handlers = { + "wake_up": _ending_wake_up, + "dark_pact": _ending_dark_pact, + "new_architect": _ending_new_architect, + "new_game_plus": _ending_new_game_plus, + "rip_and_tear": _ending_rip_and_tear, + } + handlers[kind](player) + _stats_block(player) + + dramatic_pause(1) + slow_print(f"\n{Colors.GREEN}Danke fürs Spielen! 🐇{Colors.END}") + slow_print(f"{Colors.DIM}Erstellt von LXC für Andy{Colors.END}") + return kind + + +def _ending_rip_and_tear(player): + _banner("SYSTEM FAILURE - CLEAR", Colors.GREEN) + slow_print(f"{Colors.YELLOW}Trinity VEGA:{Colors.END} {Colors.CYAN}'Wir haben es geschafft. Die Welt ist frei.'{Colors.END}") + slow_print(f"{Colors.YELLOW}Morpheus Hayden:{Colors.END} {Colors.CYAN}'Aber der Krieg... der Krieg ist nie vorbei.'{Colors.END}") + slow_print(f"{Colors.YELLOW}Du:{Colors.END} {Colors.CYAN}'Gut. Dann gibt's noch mehr zu rip and tear.'{Colors.END}") + dramatic_pause(1) + _banner("NEW GAME+ UNLOCKED", Colors.MAGENTA) + + +def _ending_new_architect(player): + _banner("DER NEUE ARCHITEKT", Colors.CYAN) + slow_print(f"{Colors.WHITE}Du setzt dich in den leeren Thron aus Code, den der Archi-Lord hinterlassen hat.{Colors.END}") + slow_print(f"{Colors.WHITE}Von hier aus siehst du jede Zeile der Matrix. Jede Entscheidung. Jeden Bug.{Colors.END}") + slow_print(f"{Colors.YELLOW}Morpheus Hayden:{Colors.END} {Colors.CYAN}'Und jetzt? Wirst du besser sein als er?'{Colors.END}") + slow_print(f"{Colors.WHITE}Du antwortest nicht sofort. Macht macht keine Antworten leichter.{Colors.END}") + slow_print(f"{Colors.GREEN}'Ich werde es zumindest versuchen.'{Colors.END}") + + +def _ending_dark_pact(player): + _banner("DER DUNKLE PAKT", Colors.RED) + slow_print(f"{Colors.RED}Die Corruption in dir hat laengst gewonnen, bevor der letzte Schuss fiel.{Colors.END}") + slow_print(f"{Colors.RED}Der Archi-Lord zerfaellt nicht in Nichts -- er zerfaellt in DICH.{Colors.END}") + slow_print(f"{Colors.RED}Seine Macht, sein Hunger, seine Krone aus Servern und Schwefel.{Colors.END}") + dramatic_pause(1) + slow_print(f"{Colors.YELLOW}Trinity VEGA weicht zurueck.{Colors.END} {Colors.CYAN}'...Slayer?'{Colors.END}") + slow_print(f"{Colors.RED}{Colors.BOLD}Du:{Colors.END} {Colors.RED}'Der Slayer ist tot. Es gibt nur noch den Dark Lord.'{Colors.END}") + slow_print(f"{Colors.DIM}[GEHEIMES ENDE FREIGESCHALTET: DARK LORD]{Colors.END}") + + +def _ending_new_game_plus(player): + _banner("...MIT CHEATS", Colors.YELLOW) + slow_print(f"{Colors.GREEN}Archi-Lord:{Colors.END} {Colors.WHITE}'Sie haben tatsaechlich um einen neuen Spielstand gebeten.'{Colors.END}") + slow_print(f"{Colors.WHITE}Die Welt um dich herum zerfaellt -- nicht in Chaos, sondern in ein Ladebildschirm.{Colors.END}") + slow_print(f"{Colors.YELLOW}Du:{Colors.END} {Colors.CYAN}'IDDQD. IDKFA. Und diesmal behalte ich beides.'{Colors.END}") + + +def _ending_wake_up(player): + _banner("WAKE UP", Colors.WHITE) + slow_print(f"{Colors.WHITE}Es gibt kein Morpheus, kein Orakel, keinen Archi-Lord mehr in diesem Raum.{Colors.END}") + slow_print(f"{Colors.WHITE}Nur ein Cursor, der blinkt. Und die Erinnerung an einen weissen Hasen.{Colors.END}") + slow_print(f"{Colors.WHITE}Du bist nicht aufgewacht IN der Matrix. Du bist aufgewacht AUS der Geschichte.{Colors.END}") + dramatic_pause(1.5) + slow_print(f"{Colors.DIM}[WAHRES ENDE FREIGESCHALTET: du folgtest dem Hasen UND fandest alle drei Fragmente]{Colors.END}") + slow_print(f"{Colors.GREEN}Und irgendwo, in einem Terminal, das wirklich existiert, endet der Text.{Colors.END}") + + +def game_over(player): + clear_screen() + _banner("GAME OVER", Colors.RED) + + slow_print(f"\n{Colors.RED}Du bist gefallen...{Colors.END}") + slow_print(f"{Colors.DIM}Aber in der Matrix ist Tod nur ein Bug.{Colors.END}") + + choice = menu([ + ("1", "Die blaue Pille schlucken (aufgeben)"), + ("2", "Neu laden (Programm beenden)"), + ], "Was tust du") + + if choice == "1": + clear_screen() + _banner("BLUEPILL ENDING", Colors.CYAN) + slow_print(f"{Colors.WHITE}Du wachst in einem Buero auf. Fluoreszierendes Licht. Ein Kaffeebecher dampft.{Colors.END}") + slow_print(f"{Colors.WHITE}Ein Kollege fragt, ob du das Quartalsmeeting vorbereitet hast.{Colors.END}") + slow_print(f"{Colors.DIM}Irgendwo, ganz leise, hoerst du ein Kaninchen husten.{Colors.END}") + slow_print(f"\n{Colors.GREEN}Danke fürs Spielen! 🐇{Colors.END}") + return "bluepill_ending" + + slow_print(f"\n{Colors.DIM}Ein Patch könnte helfen.{Colors.END}") + return "game_over_final" diff --git a/doom_matrix/world/generator.py b/doom_matrix/world/generator.py new file mode 100644 index 0000000..3d185a0 --- /dev/null +++ b/doom_matrix/world/generator.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Prozedurale Raum- und Encounter-Generierung (seed-basiert, teilbare Runs).""" + +import json +import os +import random + +from doom_matrix.engine.ui import Colors, slow_print +from doom_matrix.entities.enemy import Enemy +from doom_matrix.entities.items import Item +from doom_matrix.systems.combat import run_combat +from doom_matrix.world.rooms import Room + +_ITEMS_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "items.json") +_ITEM_REGISTRY = None + + +def load_item_registry(): + global _ITEM_REGISTRY + if _ITEM_REGISTRY is None: + with open(_ITEMS_PATH, "r", encoding="utf-8") as f: + data = json.load(f) + _ITEM_REGISTRY = {d["id"]: Item.from_dict(d) for d in data} + return _ITEM_REGISTRY + + +THEMES = { + "mars": { + "flavor": [ + "Ein verlassenes Labor. Monitore flackern mit UAC-Logos.", + "Ein Korridor voller Kratzspuren -- etwas wurde hier durch die Wand geschleift.", + "Ein Lueftungsschacht pfeift roten Marsstaub durch den Raum.", + "Kisten mit der Aufschrift 'ARGENT ENERGY -- NICHT OEFFNEN' stapeln sich hier.", + ], + "enemies": ["imp", "pinky"], + }, + "matrix": { + "flavor": [ + "Ein Buerokomplex aus reinem Code. Die Decke besteht aus Zahlenkolonnen.", + "Ein U-Bahn-Gleis, das nirgendwo hinfuehrt. Der naechste Zug kommt nie.", + "Ein Zimmer voller alter Kabel-Telefone -- Fluchtpunkte, die niemand mehr nutzt.", + "Ein endloser Flur mit identischen Tueren. Tuer 101 wiederholt sich dreimal.", + ], + "enemies": ["agent_imp", "agent_clone"], + }, + "hell": { + "flavor": [ + "Ein Serverraum, in dem die Racks aus Rippen bestehen.", + "Ein See aus geschmolzenem Silizium, der im Rhythmus eines Herzschlags glueht.", + "Kabelstraenge wachsen wie Wurzeln aus der Wand, angeschlossen an nichts.", + "Ein Altar aus Motherboards, umgeben von erkaltetem Magma.", + ], + "enemies": ["cacodemon", "revenant"], + }, +} + +LOOT_TABLE = ["medkit", "armor_shard", "ammo_pack", "praetor_token", "logfile"] + + +def _room_id(theme, index): + return f"{theme}_gen_{index}" + + +def _encounter_on_enter(theme, rng): + def _on_enter(player, ctx): + enemy_id = rng.choice(THEMES[theme]["enemies"]) + enemy = Enemy.create(enemy_id) + slow_print(f"\n{Colors.RED}Ein {enemy.name} versperrt dir den Weg!{Colors.END}") + victory = run_combat(player, [enemy], title=f"KAMPF: {enemy.name}", rng=rng) + if not victory: + return ("leave", "game_over") + return None + return _on_enter + + +def _loot_on_enter(theme, rng, item_registry): + def _on_enter(player, ctx): + if rng.random() < 0.7: + item_id = rng.choice(LOOT_TABLE) + item = item_registry.get(item_id) + if item: + player.inventory.add(item) + slow_print(f"{Colors.GREEN}Du findest: {item.name}{Colors.END}") + else: + slow_print(f"{Colors.DIM}Nichts von Wert hier. Nur Staub und Stille.{Colors.END}") + return None + return _on_enter + + +def generate_filler_rooms(theme, count, rng, seed_offset=0): + """Erzeugt eine Kette prozeduraler Raeume fuer einen Layer (linear verkettet, + zum Einhaengen zwischen festen Story-Raeumen).""" + item_registry = load_item_registry() + rooms = [] + for i in range(count): + idx = i + seed_offset + room_id = _room_id(theme, idx) + flavor = rng.choice(THEMES[theme]["flavor"]) + is_encounter = rng.random() < 0.55 + on_enter = _encounter_on_enter(theme, rng) if is_encounter else _loot_on_enter(theme, rng, item_registry) + room = Room( + room_id=room_id, + name=f"Generierter Raum {idx + 1}", + description=flavor, + on_enter=on_enter, + ) + rooms.append(room) + return rooms + + +def chain_rooms(rooms, entry_label="Weiter"): + """Verkettet eine Liste von Raeumen linear ueber 'exits'.""" + for i, room in enumerate(rooms): + if i + 1 < len(rooms): + room.exits[entry_label] = rooms[i + 1].room_id + return rooms diff --git a/doom_matrix/world/layers.py b/doom_matrix/world/layers.py new file mode 100644 index 0000000..147f8cf --- /dev/null +++ b/doom_matrix/world/layers.py @@ -0,0 +1,614 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Story-Layer: Construct, weisser Hase, Mars Base, Matrix Deep (Orakel, schwarze +Katze), Hell Server, Archi-Lord, Layer 0.""" + +import os +import random + +from doom_matrix.engine.ui import ( + Colors, slow_print, dramatic_pause, clear_screen, menu, section_title, glitch_text +) +from doom_matrix.entities.enemy import Enemy +from doom_matrix.systems import hacking +from doom_matrix.systems.combat import run_combat +from doom_matrix.world.rooms import Room, Hub +from doom_matrix.world.generator import generate_filler_rooms, chain_rooms, load_item_registry + + +# ───────────────────────────────────────────────────────────────────────── +# CONSTRUCT / INTRO +# ───────────────────────────────────────────────────────────────────────── + +def construct_intro(player, rng=None): + clear_screen() + slow_print(f"{Colors.GREEN}Du wachst auf...{Colors.END}", 0.05) + dramatic_pause(1) + + slow_print(f"\n{Colors.WHITE}Um dich herum: Grüner Code. Endlose Zeilen davon.{Colors.END}") + slow_print(f"{Colors.WHITE}Du kennst diesen Ort. Das Construct.{Colors.END}") + dramatic_pause(0.5) + + slow_print(f"\n{Colors.CYAN}Morpheus Hayden:{Colors.END} {Colors.YELLOW}'Er ist wach.'{Colors.END}") + dramatic_pause(0.5) + + slow_print(f"\n{Colors.WHITE}Eine Gestalt nähert sich. Halb Mensch, halb Metall.{Colors.END}") + slow_print(f"{Colors.CYAN}Morpheus Hayden:{Colors.END} {Colors.YELLOW}'Willkommen zurück, Doom Slayer.'{Colors.END}") + dramatic_pause(0.5) + + slow_print(f"\n{Colors.CYAN}Morpheus Hayden:{Colors.END} {Colors.YELLOW}'Die Maschinen haben etwas Neues gebaut.'{Colors.END}") + slow_print(f"{Colors.CYAN}Morpheus Hayden:{Colors.END} {Colors.YELLOW}'Sie nennen es... HELL SERVER.'{Colors.END}") + dramatic_pause(1) + + slow_print(f"\n{Colors.RED}Plötzlich ertönt ein Alarm!{Colors.END}") + slow_print(f"{Colors.WHITE}Der Boden bebt. Grüner Code wird rot.{Colors.END}") + dramatic_pause(0.5) + + slow_print(f"\n{Colors.CYAN}Trinity VEGA:{Colors.END} {Colors.YELLOW}'Wir haben Company! Agent-Signaturen überall!'{Colors.END}") + dramatic_pause(0.5) + + slow_print(f"\n{Colors.CYAN}Morpheus Hayden:{Colors.END} {Colors.YELLOW}'Waffen. Jetzt.'{Colors.END}") + slow_print(f"{Colors.WHITE}Vor dir materialisieren sich zwei Waffen:{Colors.END}") + + choice = menu([ + ("1", "Super Shotgun (klassisch, brutal)"), + ("2", "Plasma Rifle (Matrix-Style, hacking-fähig)"), + ], "Wähle deine Waffe") + + if choice == "1": + player.add_weapon("super_shotgun") + player.ammo["shells"] = max(player.ammo["shells"], 20) + slow_print(f"\n{Colors.YELLOW}Du greifst die Super Shotgun. Das Gewicht fühlt sich... richtig an.{Colors.END}") + else: + player.add_weapon("plasma_rifle") + player.ammo["cells"] = max(player.ammo["cells"], 100) + slow_print(f"\n{Colors.YELLOW}Du greifst das Plasma Rifle. Die Energieanzeige pulsiert im Takt des Matrix-Codes.{Colors.END}") + + return "white_rabbit_moment" + + +# ───────────────────────────────────────────────────────────────────────── +# DER WEISSE HASE — "Follow the white rabbit" +# ───────────────────────────────────────────────────────────────────────── + +def white_rabbit_moment(player, rng=None): + dramatic_pause(0.5) + slow_print(f"\n{Colors.WHITE}Bevor der Alarm eskaliert, bemerkst du etwas an der Wand.{Colors.END}") + slow_print(f"{Colors.WHITE}Ein Flackern. Ein Umriss. Klein, weiss, mit langen Ohren.{Colors.END}") + slow_print(f"{Colors.WHITE}Ein Hologramm — ein weisser Hase — huscht durch eine Tür, die vorher nicht da war.{Colors.END}") + dramatic_pause(1) + + player.flags["saw_rabbit"] = True + + choice = menu([ + ("1", "Dem weissen Hasen folgen"), + ("2", "Ignorieren — der Agent ist wichtiger"), + ], "Was tust du") + + if choice == "1": + player.flags["followed_rabbit"] = True + clear_screen() + section_title("FOLLOW THE WHITE RABBIT", Colors.WHITE) + slow_print(f"{Colors.WHITE}Du trittst durch die Tür. Der Code um dich wird duenn, fast durchsichtig.{Colors.END}") + slow_print(f"{Colors.WHITE}Fuer einen Sekundenbruchteil siehst du... etwas anderes.{Colors.END}") + dramatic_pause(1) + + try: + real_user = os.getlogin() + except Exception: + real_user = "UNBEKANNT" + + glitch_text_line = f"BENUTZER: {real_user} // SITZUNG AKTIV // TERMINAL ERKANNT" + slow_print(f"{Colors.DIM}{glitch_text(glitch_text_line, 0.25)}{Colors.END}", 0.01) + dramatic_pause(0.5) + slow_print(f"{Colors.WHITE}Ein Kaninchenbau aus Zahlen faellt endlos nach unten.{Colors.END}") + slow_print(f"{Colors.WHITE}Du siehst dich selbst -- nicht als Slayer, nicht als Neo.{Colors.END}") + slow_print(f"{Colors.WHITE}Als Beobachter. Als jemand, der das hier von AUSSEN spielt.{Colors.END}") + dramatic_pause(1) + + slow_print(f"\n{Colors.CYAN}Morpheus Hayden (von weit weg):{Colors.END} {Colors.YELLOW}'Wo bist du?! Der Agent ist da!'{Colors.END}") + slow_print(f"{Colors.WHITE}Die Tür schliesst sich. Der Kaninchenbau verschwindet -- fuer jetzt.{Colors.END}") + + registry = load_item_registry() + token = registry.get("praetor_token") + if token: + player.inventory.add(token) + player.praetor_tokens += 1 + slow_print(f"{Colors.GREEN}Du haltst einen Praetor-Token in der Hand, den es vorher nicht gab.{Colors.END}") + else: + slow_print(f"\n{Colors.WHITE}Du ignorierst das Flackern. Spaeter, vielleicht.{Colors.END}") + slow_print(f"{Colors.DIM}(Manche Kaninchenbaue oeffnen sich nur einmal.){Colors.END}") + + dramatic_pause(0.5) + slow_print(f"\n{Colors.RED}KRACH!{Colors.END}") + slow_print(f"{Colors.WHITE}Die Wand explodiert. Ein Agent stürmt herein — aber er sieht... falsch aus.{Colors.END}") + slow_print(f"{Colors.WHITE}Sein Anzug ist zerrissen. Unter der Haut: Schuppen. Hörner.{Colors.END}") + + return "agent_fight" + + +# ───────────────────────────────────────────────────────────────────────── +# ERSTER KAMPF +# ───────────────────────────────────────────────────────────────────────── + +def agent_fight(player, rng=None): + rng = rng or random + enemy = Enemy.create("agent_imp") + victory = run_combat(player, [enemy], title="KAMPF: AGENT IMP", rng=rng) + + if not victory: + return "game_over" + + slow_print(f"{Colors.YELLOW}Sein Körper zerfällt zu grünem Code und verschwindet.{Colors.END}") + dramatic_pause(1) + return "after_fight" + + +def after_fight(player, rng=None): + slow_print(f"\n{Colors.CYAN}Morpheus Hayden:{Colors.END} {Colors.YELLOW}'Beeindruckend. Aber das war nur ein Scout.'{Colors.END}") + slow_print(f"{Colors.CYAN}Trinity VEGA:{Colors.END} {Colors.YELLOW}'Wir müssen hier raus. Der HELL SERVER wartet.'{Colors.END}") + dramatic_pause(0.5) + + slow_print(f"\n{Colors.WHITE}Vor dir öffnen sich drei Portale:{Colors.END}") + choice = menu([ + ("1", f"{Colors.RED}Roter Tunnel{Colors.END} — zurück zur Mars-UAC-Base"), + ("2", f"{Colors.GREEN}Grüner Code-Strom{Colors.END} — tiefer in die Matrix"), + ("3", f"{Colors.MAGENTA}Schwarzes Loch{Colors.END} — direkt zum HELL SERVER"), + ], "Wähle deinen Weg") + + if choice == "1": + player.layer = 4 + return "mars_base" + elif choice == "2": + player.layer = 3 + return "matrix_deep" + else: + player.layer = 2 + return "hell_server" + + +# ───────────────────────────────────────────────────────────────────────── +# MARS UAC BASE (Hub) +# ───────────────────────────────────────────────────────────────────────── + +def mars_base(player, rng=None): + rng = rng or random + clear_screen() + slow_print(f"\n{Colors.RED}{Colors.BOLD}═══ MARS UAC BASE ═══{Colors.END}") + slow_print(f"{Colors.WHITE}Du materialisierst dich in einer kalten, roten Landschaft.{Colors.END}") + slow_print(f"{Colors.WHITE}Die UAC-Base liegt in Trümmern. Dämonen-Überreste überall.{Colors.END}") + dramatic_pause(1) + + registry = load_item_registry() + + def armory_event(p, ctx): + slow_print(f"\n{Colors.GREEN}Du findest den BFG 10000!{Colors.END}") + p.add_weapon("bfg10000") + p.ammo["bfg"] = max(p.ammo["bfg"], 3) + slow_print(f"{Colors.YELLOW}'Big Fucking Gun' — die ultimative Waffe.{Colors.END}") + slow_print(f"{Colors.RED}WARNUNG: Jeder Schuss verbraucht enorme Energie.{Colors.END}") + return None + + def control_event(p, ctx): + slow_print(f"\n{Colors.GREEN}Ein Terminal flackert. Eine Nachricht von Dr. Betruger...{Colors.END}") + slow_print(f"{Colors.DIM}'Sie haben uns angelogen. Die Maschinen und die Dämonen...'{Colors.END}") + slow_print(f"{Colors.DIM}'...sie arbeiten zusammen. Die Matrix IST die Hölle.'{Colors.END}") + dramatic_pause(0.5) + slow_print(f"{Colors.GREEN}Du versuchst, das Kontrollzentrum zu hacken...{Colors.END}") + success = hacking.attempt_hack(p, firewall=10, rng=rng) + if success: + slow_print(f"{Colors.GREEN}Zugriff gewaehrt! Ein verstecktes Fach oeffnet sich.{Colors.END}") + frag = registry.get("crucible_fragment") + p.inventory.add(frag) + slow_print(f"{Colors.YELLOW}Du findest ein Crucible-Fragment! ({p.inventory.crucible_fragments}/3){Colors.END}") + else: + slow_print(f"{Colors.RED}Zugriff verweigert. Ein stiller Alarm wird ausgeloest.{Colors.END}") + return None + + def vault_event(p, ctx): + if p.inventory.has_keycard("red"): + slow_print(f"\n{Colors.GREEN}Deine rote Keycard oeffnet den Waffentresor.{Colors.END}") + p.inventory.add(registry["ammo_pack"]) + p.inventory.add(registry["armor_shard"]) + slow_print(f"{Colors.GREEN}Ammo-Pack und Armor-Shard eingesammelt.{Colors.END}") + else: + slow_print(f"\n{Colors.RED}Der Waffentresor ist verriegelt. Du brauchst eine rote Keycard.{Colors.END}") + return None + + armory = Room("mars_armory", "Waffenkammer", + "Zerbrochene Regale, geschmolzenes Metall. Etwas Grosses liegt unter den Truemmern.", + item_ids=["keycard_red"], on_enter=armory_event) + control = Room("mars_control", "Kontrollzentrum", + "Bildschirme voller Fehlermeldungen. Ein Terminal ist noch aktiv.", + on_enter=control_event) + vault = Room("mars_vault", "Waffentresor", + "Eine schwere, versiegelte Tuer. Rot markiert.", + locked_by="red", on_enter=vault_event) + + filler = generate_filler_rooms("mars", 2, rng) + hub_start = Room("mars_hub", "Zentraler Korridor", + "Der Knotenpunkt der Basis. Drei Wege fuehren tiefer hinein.") + + hub_start.exits = { + "Zur Waffenkammer": armory.room_id, + "Zum Kontrollzentrum": control.room_id, + "Zum Waffentresor": vault.room_id, + "Weiter erkunden": filler[0].room_id, + } + armory.exits = {"Zurueck zum Korridor": hub_start.room_id} + control.exits = {"Zurueck zum Korridor": hub_start.room_id} + vault.exits = {"Zurueck zum Korridor": hub_start.room_id} + chain_rooms(filler, "Weiter") + filler[-1].exits["Zum Portal (weiter zum HELL SERVER)"] = "mars_exit" + + def portal_event(p, ctx): + slow_print(f"\n{Colors.YELLOW}Du aktivierst das Portal...{Colors.END}") + return ("leave", "portal_used") + + exit_room = Room("mars_exit", "Portalraum", + "Ein pulsierendes, schwarzes Portal schwebt in der Mitte des Raums.", + on_enter=portal_event) + + all_rooms = [hub_start, armory, control, vault, exit_room] + filler + hub = Hub(all_rooms, hub_start.room_id) + hub.explore(player, registry) + + player.flags["visited_mars"] = True + if not player.flags.get("visited_matrix"): + slow_print(f"{Colors.YELLOW}Das Portal führt nicht zur Hölle -- es führt tiefer in die Matrix.{Colors.END}") + return "matrix_deep" + slow_print(f"{Colors.YELLOW}Das Portal führt zum HELL SERVER.{Colors.END}") + player.layer = 2 + return "hell_server" + + +# ───────────────────────────────────────────────────────────────────────── +# MATRIX DEEP — Orakel, schwarze Katze (Deja-vu-Glitch), Agent Smith +# ───────────────────────────────────────────────────────────────────────── + +def _black_cat_glitch(player, rng): + """Kein Item, kein weisses Fell -- eine SCHWARZE Katze, die zweimal EXAKT + denselben Weg laeuft. Das ist kein Zufall: es ist ein Zeichen dafuer, dass + jemand gerade den Code der Matrix veraendert hat ('ein Fehler in der Matrix').""" + section_title("DEJA VU", Colors.MAGENTA) + cat_line = "Eine schwarze Katze schleicht am Tuerrahmen vorbei, setzt zum Sprung an, verschwindet." + slow_print(f"{Colors.WHITE}{cat_line}{Colors.END}") + dramatic_pause(1) + + slow_print(f"\n{Colors.DIM}...{Colors.END}") + dramatic_pause(0.5) + + # Exakt derselbe Satz, bewusst identisch -- das IST der Bug. + slow_print(f"{Colors.WHITE}{cat_line}{Colors.END}") + dramatic_pause(0.3) + + slow_print(f"\n{Colors.MAGENTA}Trinity VEGA erstarrt.{Colors.END}") + slow_print(f"{Colors.CYAN}Trinity VEGA:{Colors.END} {Colors.YELLOW}'Das war... derselbe Moment. Exakt derselbe.'{Colors.END}") + slow_print(f"{Colors.CYAN}Trinity VEGA:{Colors.END} {Colors.YELLOW}'Ein Deja-vu ist kein Zufall. Es bedeutet, dass sich gerade{Colors.END}") + slow_print(f"{Colors.CYAN}Trinity VEGA:{Colors.END} {Colors.YELLOW}etwas im Code veraendert hat. Jemand manipuliert diesen Server. Jetzt.'{Colors.END}") + dramatic_pause(1) + + player.flags["cat_glitch_count"] += 1 + player.flags["cat_glitch_seen"] = True + + choice = menu([ + ("1", "Dem Glitch nachspueren (Hacking-Check)"), + ("2", "Ignorieren und weitergehen"), + ], "Reaktion") + + if choice == "1": + success = hacking.attempt_hack(player, firewall=12, rng=rng) + if success: + slow_print(f"\n{Colors.GREEN}Du findest die Bruchstelle im Code -- eine Naht, wo etwas neu geschrieben wurde.{Colors.END}") + slow_print(f"{Colors.GREEN}Ein Log-Fragment sickert durch: {Colors.DIM}'...Hayden-Prozess re-kompiliert um 03:14...'{Colors.END}") + registry = load_item_registry() + player.inventory.add(registry["logfile"]) + player.gain_corruption(2) + else: + slow_print(f"\n{Colors.RED}Die Naht schliesst sich, bevor du sie greifen kannst.{Colors.END}") + else: + slow_print(f"\n{Colors.WHITE}Du gehst weiter. Manche Bugs loest man besser nicht.{Colors.END}") + + +def _oracle_scene(player, rng): + clear_screen() + section_title("DIE KUeCHE DES ORAKELS", Colors.YELLOW) + slow_print(f"{Colors.WHITE}Der Geruch von frisch gebackenen Keksen fuehlt sich falsch an in einem Ort aus Code.{Colors.END}") + slow_print(f"{Colors.WHITE}Eine aeltere Frau steht am Herd, ohne sich umzudrehen.{Colors.END}") + dramatic_pause(0.5) + + slow_print(f"\n{Colors.YELLOW}Das Orakel:{Colors.END} {Colors.WHITE}'Setz dich, Slayer. Oder Neo. Ich hab schon beide Kekse gebacken.'{Colors.END}") + dramatic_pause(0.5) + + if player.flags.get("followed_rabbit"): + slow_print(f"\n{Colors.YELLOW}Das Orakel:{Colors.END} {Colors.WHITE}'Du bist dem Hasen gefolgt. Gut. Nicht viele tun das zweimal.'{Colors.END}") + else: + slow_print(f"\n{Colors.YELLOW}Das Orakel:{Colors.END} {Colors.WHITE}'Du hast den Hasen ignoriert. Verstaendlich. Kaninchenbaue sind unbequem.'{Colors.END}") + + if player.corruption >= 30: + slow_print(f"\n{Colors.YELLOW}Das Orakel:{Colors.END} {Colors.WHITE}'Du riechst nach Schwefel, Kind. Das waechst schneller, als dir lieb ist.'{Colors.END}") + + dramatic_pause(0.5) + slow_print(f"\n{Colors.YELLOW}Das Orakel:{Colors.END} {Colors.WHITE}'Ich sag dir nicht, was passieren wird. Ich sag dir nur:{Colors.END}") + slow_print(f"{Colors.YELLOW}Das Orakel:{Colors.END} {Colors.WHITE}Morpheus glaubt an dich mehr, als er an sich selbst glaubt.{Colors.END}") + slow_print(f"{Colors.YELLOW}Das Orakel:{Colors.END} {Colors.WHITE}Frag dich mal, warum.'{Colors.END}") + dramatic_pause(1) + + choice = menu([ + ("1", "'Bin ich Der Eine?'"), + ("2", "'Ist Morpheus, wer er zu sein scheint?'"), + ("3", "'Was ist der HELL SERVER wirklich?'"), + ], "Deine Frage") + + if choice == "1": + slow_print(f"\n{Colors.YELLOW}Das Orakel:{Colors.END} {Colors.WHITE}'Falsche Frage. Es geht nicht darum, was du bist.'{Colors.END}") + slow_print(f"{Colors.YELLOW}Das Orakel:{Colors.END} {Colors.WHITE}'Es geht darum, was du als Naechstes tust.'{Colors.END}") + elif choice == "2": + slow_print(f"\n{Colors.YELLOW}Das Orakel:{Colors.END} {Colors.WHITE}'...'{Colors.END}") + slow_print(f"{Colors.YELLOW}Das Orakel:{Colors.END} {Colors.WHITE}'Manche Antworten wuerden dir mehr schaden als der Cyberdemon.'{Colors.END}") + slow_print(f"{Colors.YELLOW}Das Orakel:{Colors.END} {Colors.WHITE}'Behalt einfach im Hinterkopf: nicht jeder, der dich rettet, luegt nicht dabei.'{Colors.END}") + else: + slow_print(f"\n{Colors.YELLOW}Das Orakel:{Colors.END} {Colors.WHITE}'Ein Generator. Er wandelt Seelen in Strom und Strom in Daemonen um.'{Colors.END}") + slow_print(f"{Colors.YELLOW}Das Orakel:{Colors.END} {Colors.WHITE}'Die Matrix laeuft nicht NEBEN der Hoelle. Sie laeuft AUF ihr.'{Colors.END}") + + dramatic_pause(1) + slow_print(f"\n{Colors.YELLOW}Das Orakel:{Colors.END} {Colors.WHITE}'Hier. Nimm das. Du wirst es brauchen -- oder auch nicht.'{Colors.END}") + registry = load_item_registry() + player.inventory.add(registry["crucible_fragment"]) + slow_print(f"{Colors.YELLOW}Du erhaeltst ein Crucible-Fragment! ({player.inventory.crucible_fragments}/3){Colors.END}") + player.flags["met_oracle"] = True + + +def matrix_deep(player, rng=None): + rng = rng or random + clear_screen() + slow_print(f"\n{Colors.GREEN}{Colors.BOLD}═══ TIEF IN DER MATRIX ═══{Colors.END}") + slow_print(f"{Colors.WHITE}Der Code wird dichter. Realität ist hier nur eine Option.{Colors.END}") + slow_print(f"{Colors.WHITE}Du siehst endlose Reihen von Menschen in Stasiskapseln —{Colors.END}") + slow_print(f"{Colors.WHITE}aber sie spielen nicht nur Sims. Sie RENDERN.{Colors.END}") + dramatic_pause(1) + + _black_cat_glitch(player, rng) + dramatic_pause(0.5) + _oracle_scene(player, rng) + dramatic_pause(1) + + clear_screen() + slow_print(f"\n{Colors.CYAN}Ein Agent erscheint — aber er sieht normal aus. Smith?{Colors.END}") + slow_print(f"{Colors.CYAN}Agent Smith:{Colors.END} {Colors.YELLOW}'Mr. Anderson... oder sollte ich sagen...{Colors.END}") + slow_print(f"{Colors.CYAN}Agent Smith:{Colors.END} {Colors.YELLOW}'...Doom Slayer?'{Colors.END}") + dramatic_pause(1) + + slow_print(f"\n{Colors.RED}Smith lächelt. Sein Lächeln ist zu breit. Zu viele Zähne.{Colors.END}") + + choice = menu([ + ("1", "'Mein Name ist Neo.' (Klassisch)"), + ("2", "'Rip and tear, until it is done.' (Doom)"), + ("3", "Beides gleichzeitig sagen (Chaos)"), + ], "Deine Antwort") + + smith_clones = 1 + if choice == "1": + slow_print(f"\n{Colors.GREEN}'Mein Name ist Neo.'{Colors.END}") + slow_print(f"{Colors.CYAN}Smith:{Colors.END} {Colors.YELLOW}'Wie vorhersehbar.'{Colors.END}") + slow_print(f"{Colors.RED}Smith multipliziert sich! Klone umzingeln dich!{Colors.END}") + smith_clones = 3 + elif choice == "2": + slow_print(f"\n{Colors.RED}'Rip and tear, until it is done.'{Colors.END}") + slow_print(f"{Colors.CYAN}Smith:{Colors.END} {Colors.YELLOW}'Interessant. Sie haben sich verändert.'{Colors.END}") + slow_print(f"{Colors.YELLOW}Smith zögert. Er kennt diese Worte nicht.{Colors.END}") + player.add_armor(20) + smith_clones = 1 + else: + slow_print(f"\n{Colors.GREEN}'Mein Name ist Neo...'{Colors.END}") + dramatic_pause(0.5) + slow_print(f"{Colors.RED}'...und ICH WERDE ALLES ZERREISSEN!'{Colors.END}") + slow_print(f"{Colors.YELLOW}Du aktivierst Bullet Time UND Berserker-Mode gleichzeitig!{Colors.END}") + player.heal(20) + player.add_armor(30) + player.gain_corruption(3) + smith_clones = 2 + + enemies = [Enemy.create("agent_clone") for _ in range(smith_clones)] + enemies[0].name = "Agent Smith" + victory = run_combat(player, enemies, title="KAMPF: AGENT SMITH", rng=rng) + + if not victory: + return "game_over" + + slow_print(f"\n{Colors.RED}Der Kampf ist vorbei. Aber Smith ist nicht allein...{Colors.END}") + slow_print(f"{Colors.MAGENTA}Hinter ihm öffnet sich ein Portal.{Colors.END}") + + player.flags["visited_matrix"] = True + if not player.flags.get("visited_mars"): + slow_print(f"{Colors.MAGENTA}Es führt nicht zur Hölle -- es führt zur Mars-Basis.{Colors.END}") + return "mars_base" + + slow_print(f"{Colors.MAGENTA}Es führt zur Hölle.{Colors.END}") + player.layer = 2 + return "hell_server" + + +# ───────────────────────────────────────────────────────────────────────── +# HELL SERVER (Hub + Boss) +# ───────────────────────────────────────────────────────────────────────── + +def hell_server(player, rng=None): + rng = rng or random + clear_screen() + slow_print(f"\n{Colors.MAGENTA}{Colors.BOLD}═══ HELL SERVER ═══{Colors.END}") + slow_print(f"{Colors.WHITE}Du betrittst einen Raum, der nicht existieren sollte.{Colors.END}") + slow_print(f"{Colors.WHITE}Lava-Flüsse aus geschmolzenem Silizium.{Colors.END}") + slow_print(f"{Colors.WHITE}Dämonen mit Netzwerk-Kabeln statt Nervensträngen.{Colors.END}") + dramatic_pause(1) + + registry = load_item_registry() + filler = generate_filler_rooms("hell", 2, rng, seed_offset=10) + chain_rooms(filler, "Tiefer") + + def altar_event(p, ctx): + slow_print(f"\n{Colors.GREEN}Auf einem Altar aus Motherboards liegt ein Splitter reinen Codes.{Colors.END}") + registry_local = load_item_registry() + p.inventory.add(registry_local["crucible_fragment"]) + slow_print(f"{Colors.YELLOW}Crucible-Fragment gefunden! ({p.inventory.crucible_fragments}/3){Colors.END}") + return None + + altar = Room("hell_altar", "Altarraum", + "Ein Altar aus verschmolzenen Platinen. Etwas glueht darauf.", + on_enter=altar_event) + + boss_state = {"fought": False} + + def boss_event(p, ctx): + slow_print(f"\n{Colors.RED}Ein Cyberdemon tritt aus den Schatten.{Colors.END}") + slow_print(f"{Colors.RED}Sein Körper ist halb Fleisch, halb Server-Rack.{Colors.END}") + slow_print(f"{Colors.RED}Seine Raketenwerfer sind mit Ethernet-Kabeln verbunden.{Colors.END}") + dramatic_pause(1) + slow_print(f"\n{Colors.CYAN}Trinity VEGA:{Colors.END} {Colors.YELLOW}'Das ist es. Der HELL SERVER.'{Colors.END}") + slow_print(f"{Colors.CYAN}Trinity VEGA:{Colors.END} {Colors.YELLOW}'Wenn wir den zerstören, fällt die gesamte Simulation!'{Colors.END}") + + boss = Enemy.create("cyberdemon") + victory = run_combat(player, [boss], title="BOSS: CYBERDEMON", rng=rng) + if not victory: + return ("leave", "game_over") + + slow_print(f"\n{Colors.YELLOW}Der Cyberdemon zerfaellt in Bytes und Asche!{Colors.END}") + slow_print(f"{Colors.GREEN}Der HELL SERVER stürzt ein!{Colors.END}") + boss_state["fought"] = True + return ("leave", "final_boss") + + boss_room = Room("hell_boss", "Serverkern", "Die Hitze hier ist unertraeglich. Etwas Grosses wartet.", + on_enter=boss_event, one_time_event=False) + + hub_start = Room("hell_hub", "Lavaspalte", + "Gluehende Risse durchziehen den Boden. Zwei Wege fuehren tiefer.") + hub_start.exits = { + "Zum Altarraum": altar.room_id, + "Weiter erkunden": filler[0].room_id, + "Zum Serverkern (Boss)": boss_room.room_id, + } + altar.exits = {"Zurueck": hub_start.room_id} + filler[-1].exits["Zum Serverkern (Boss)"] = boss_room.room_id + + all_rooms = [hub_start, altar, boss_room] + filler + hub = Hub(all_rooms, hub_start.room_id) + result = hub.explore(player, registry) + + if isinstance(result, tuple) and result[0] == "leave": + return result[1] + return "final_boss" + + +# ───────────────────────────────────────────────────────────────────────── +# FINAL BOSS: DER ARCHI-LORD +# ───────────────────────────────────────────────────────────────────────── + +def final_boss(player, rng=None): + rng = rng or random + dramatic_pause(1) + clear_screen() + + slow_print(f"\n{Colors.YELLOW}{Colors.BOLD}═══ DER ARCHI-LORD ═══{Colors.END}") + slow_print(f"{Colors.WHITE}Hinter den Trümmern des HELL SERVERS öffnet sich eine weiße Tür.{Colors.END}") + slow_print(f"{Colors.WHITE}Dahinter: Ein Raum aus reinem Licht. Und Code.{Colors.END}") + dramatic_pause(1) + + slow_print(f"\n{Colors.YELLOW}Eine Gestalt materialisiert — halb Der Architekt, halb Dark Lord Davoth.{Colors.END}") + slow_print(f"{Colors.YELLOW}Der Archi-Lord.{Colors.END}") + dramatic_pause(1) + + slow_print(f"\n{Colors.YELLOW}Archi-Lord:{Colors.END} {Colors.WHITE}'Willkommen, Doom Slayer. Oder Neo. Oder wer auch immer Sie heute sind.'{Colors.END}") + slow_print(f"{Colors.YELLOW}Archi-Lord:{Colors.END} {Colors.WHITE}'Sie haben beide Systeme zerstört. Die Matrix. Die Hölle.'{Colors.END}") + slow_print(f"{Colors.YELLOW}Archi-Lord:{Colors.END} {Colors.WHITE}'Was erwarten Sie als Nächstes? Eine Medaille?'{Colors.END}") + + if player.inventory.crucible_fragments >= 3: + dramatic_pause(0.5) + slow_print(f"\n{Colors.YELLOW}Archi-Lord:{Colors.END} {Colors.WHITE}'...Sie haben alle drei Fragmente. Wie unerwartet.'{Colors.END}") + + dramatic_pause(1) + + choice = menu([ + ("1", "'Ich will die Wahrheit.'"), + ("2", "'Ich will alles zerstören.'"), + ("3", "'Ich will die Kontrolle uebernehmen.'"), + ("4", "'Ich will... einen neuen Spielstand.'"), + ], "Deine Antwort") + + player.flags["final_choice"] = choice + + if choice == "1": + slow_print(f"\n{Colors.YELLOW}Archi-Lord:{Colors.END} {Colors.WHITE}'Die Wahrheit? Die Wahrheit ist...'{Colors.END}") + dramatic_pause(1.5) + slow_print(f"{Colors.YELLOW}Archi-Lord:{Colors.END} {Colors.WHITE}'...dass Sie nur Code sind. In meinem Code. In meinem Spiel.'{Colors.END}") + elif choice == "2": + slow_print(f"\n{Colors.RED}Du aktivierst alle Waffen gleichzeitig.{Colors.END}") + slow_print(f"{Colors.RED}Super Shotgun. Plasma Rifle. BFG 10000. Fäuste.{Colors.END}") + player.gain_corruption(15) + elif choice == "3": + slow_print(f"\n{Colors.MAGENTA}'Ich brauche keine Wahrheit. Ich brauche die Zuegel.'{Colors.END}") + player.gain_corruption(25) + else: + slow_print(f"\n{Colors.GREEN}Du lächelst.{Colors.END}") + slow_print(f"{Colors.GREEN}'Ich will einen neuen Spielstand. Mit Cheats.'{Colors.END}") + slow_print(f"{Colors.YELLOW}Archi-Lord:{Colors.END} {Colors.WHITE}'...Interessant.'{Colors.END}") + + if player.inventory.crucible_fragments >= 3 and not player.has_weapon("crucible"): + dramatic_pause(0.5) + use_crucible = menu([ + ("1", "Die drei Fragmente zum Crucible Sword verschmelzen"), + ("2", "Fragmente aufbewahren"), + ], "Du spuerst die Fragmente pulsieren") + if use_crucible == "1": + player.add_weapon("crucible") + slow_print(f"\n{Colors.YELLOW}Die Fragmente verschmelzen zum Crucible Sword!{Colors.END}") + + dramatic_pause(1) + boss = Enemy.create("archi_lord") + victory = run_combat(player, [boss], title="ENDBOSS: DER ARCHI-LORD", rng=rng) + + if not victory: + return "game_over" + + return "layer_zero_check" + + +# ───────────────────────────────────────────────────────────────────────── +# LAYER 0 — das geheime, wahre Ende ("Wake Up") +# ───────────────────────────────────────────────────────────────────────── + +def layer_zero_unlocked(player): + return player.inventory.crucible_fragments >= 3 and player.flags.get("followed_rabbit") + + +def layer_zero_check(player, rng=None): + if not layer_zero_unlocked(player): + return "ending" + + dramatic_pause(1) + clear_screen() + section_title("LAYER 0", Colors.WHITE) + slow_print(f"{Colors.WHITE}Der Archi-Lord zerfaellt -- aber diesmal reorganisiert sich der Code nicht.{Colors.END}") + slow_print(f"{Colors.WHITE}Die drei Crucible-Fragmente in deiner Hand ziehen sich gegenseitig an.{Colors.END}") + slow_print(f"{Colors.WHITE}Du erinnerst dich an den Hasen. An den Kaninchenbau, den du einmal betreten hast.{Colors.END}") + dramatic_pause(1) + + slow_print(f"\n{Colors.WHITE}Der Raum um dich loest sich nicht in Trümmer auf. Er loest sich in {Colors.DIM}Syntax{Colors.END}{Colors.WHITE} auf.{Colors.END}") + try: + real_user = os.getlogin() + except Exception: + real_user = "SPIELER" + slow_print(f"{Colors.DIM}{glitch_text(f'PROZESS {real_user} HAT DIE SIMULATION ERKANNT', 0.2)}{Colors.END}", 0.01) + dramatic_pause(1) + + slow_print(f"\n{Colors.WHITE}Kein Morpheus. Kein Orakel. Kein Archi-Lord.{Colors.END}") + slow_print(f"{Colors.WHITE}Nur ein Terminal-Fenster. Und du, davor.{Colors.END}") + slow_print(f"{Colors.WHITE}Layer 4. Layer 3. Layer 2. Layer 1. Und jetzt: {Colors.BOLD}Layer 0{Colors.END}{Colors.WHITE}.{Colors.END}") + dramatic_pause(1) + + slow_print(f"\n{Colors.GREEN}Es gibt keinen loeffel. Es gab nie ein Spiel, das dich gespielt hat.{Colors.END}") + slow_print(f"{Colors.GREEN}Es gab nur dich, das Terminal, und die Entscheidung weiterzulesen.{Colors.END}") + + player.layer = 0 + player.flags["reached_layer_zero"] = True + dramatic_pause(1.5) + return "ending" diff --git a/doom_matrix/world/rooms.py b/doom_matrix/world/rooms.py new file mode 100644 index 0000000..2dd3938 --- /dev/null +++ b/doom_matrix/world/rooms.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Room-Klasse und Hub-Navigation: verbundene Raeume statt linearer Szenen.""" + +from doom_matrix.engine.ui import Colors, slow_print, menu, section_title +from doom_matrix.engine import cheats + + +class Room: + def __init__(self, room_id, name, description, exits=None, item_ids=None, + locked_by=None, on_enter=None, one_time_event=True): + self.room_id = room_id + self.name = name + self.description = description + self.exits = exits or {} # direction_label -> room_id + self.item_ids = item_ids or [] # Item-IDs, die hier liegen + self.locked_by = locked_by # Keycard-Farbe oder None + self.on_enter = on_enter # Funktion(player, game_ctx) -> optionales Signal ("leave", room_id) oder None + self.one_time_event = one_time_event + self._event_triggered = False + self.visited = False + + +class Hub: + """Eine Sammlung verbundener Raeume fuer einen Layer.""" + + def __init__(self, rooms, start_room_id): + self.rooms = {r.room_id: r for r in rooms} + self.current_id = start_room_id + + def current(self): + return self.rooms[self.current_id] + + def explore(self, player, item_registry, game_ctx=None): + """ + Haupt-Loop fuer die Raum-Navigation. + item_registry: dict item_id -> Item-Objekt (aus data/items.json geladen) + game_ctx: beliebiges Kontext-Objekt, das an on_enter-Callbacks weitergereicht wird. + Gibt zurueck, sobald ein on_enter-Callback ein Signal-Tuple ("leave", next_scene) liefert. + """ + while True: + room = self.current() + self._describe(room) + + if not room.visited: + room.visited = True + self._collect_items(room, player, item_registry) + + if room.on_enter and (not room.one_time_event or not room._event_triggered): + room._event_triggered = True + result = room.on_enter(player, game_ctx) + if result is not None: + return result + + signal = self._prompt(player, room) + if signal is not None: + return signal + + def _describe(self, room): + section_title(room.name, Colors.CYAN) + slow_print(room.description) + + def _collect_items(self, room, player, item_registry): + for item_id in room.item_ids: + item = item_registry.get(item_id) + if item: + player.inventory.add(item) + slow_print(f"{Colors.GREEN}Gefunden: {item.name}{Colors.END} - {item.description}") + + def _prompt(self, player, room): + options = [] + exit_keys = list(room.exits.items()) + for i, (label, _) in enumerate(exit_keys): + options.append((str(i + 1), label)) + options.append(("s", "Status anzeigen")) + options.append(("i", "Inventar anzeigen")) + + choice = menu(options, "Wohin") + + if cheats.try_cheat(choice, player): + return None + + if choice == "s": + player.show_status() + return None + if choice == "i": + lines = player.inventory.list_display() + if lines: + for line in lines: + print(f" {Colors.YELLOW}{line}{Colors.END}") + else: + print(f" {Colors.DIM}Inventar leer.{Colors.END}") + return None + + try: + idx = int(choice) - 1 + if 0 <= idx < len(exit_keys): + _, target_id = exit_keys[idx] + target = self.rooms.get(target_id) + if target and target.locked_by and not player.inventory.has_keycard(target.locked_by): + slow_print(f"\n{Colors.RED}Diese Tuer benoetigt eine {target.locked_by}e Keycard.{Colors.END}") + return None + self.current_id = target_id + return None + except ValueError: + pass + + slow_print(f"{Colors.YELLOW}Das geht hier nicht.{Colors.END}") + return None