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:
commit
46116f7f4b
25 files changed
+2594
No files matched your search
@@ -0,0 +1,3 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
doom_matrix/saves/*.json
|
||||
@@ -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.* 🐇
|
||||
@@ -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()
|
||||
Whitespace-only changes.
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
Whitespace-only changes.
@@ -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
|
||||
@@ -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)
|
||||
@@ -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.'"
|
||||
@@ -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()
|
||||
Whitespace-only changes.
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
Whitespace-only changes.
@@ -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
|
||||
@@ -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
|
||||
Whitespace-only changes.
@@ -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"
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
Reference in new issue
Block a user