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).
This commit is contained in:
fedora_openclaw committed 2026-07-05 18:56:44 +02:00
commit 46116f7f4b
25 files changed
+2594

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
__pycache__/
*.pyc
doom_matrix/saves/*.json
+250
View File
@@ -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=<zahl>` | 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("<id>")` 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.* 🐇
+13
View File
@@ -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()
View File
Whitespace-only changes.
+86
View File
@@ -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
}
]
+82
View File
@@ -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
}
]
+47
View File
@@ -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."
}
]
View File
Whitespace-only changes.
+63
View File
@@ -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
+132
View File
@@ -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)
+85
View File
@@ -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.'"
+113
View File
@@ -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()
View File
Whitespace-only changes.
+67
View File
@@ -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
+125
View File
@@ -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
+232
View File
@@ -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
+17
View File
@@ -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()
View File
Whitespace-only changes.
+233
View File
@@ -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
+72
View File
@@ -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
View File
Whitespace-only changes.
+134
View File
@@ -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"
+117
View File
@@ -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
+614
View File
@@ -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"
+109
View File
@@ -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