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
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()
|
||||
Reference in new issue
Block a user