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