# 10 — Raum-Prefab-Format Type: grilling Status: resolved Blocked by: 03 ## Question Wie sieht ein Raum-Prefab in TOML aus, und wie fügt der Generator daraus eine Etage zusammen? Entschieden ist der Hybrid: Modder bauen Prefabs, der Generator setzt prozedural zusammen. Zu klären: - Prefab-Inhalt: Grid als ASCII-Block oder als Koordinatenliste? Wie werden Ausgänge markiert? Wie Spawn-Punkte für Gegner, Loot, NPCs, Treppen? - Wie flexibel ist die Größe — feste Raster oder beliebige Rechtecke, Rotation und Spiegelung erlaubt? - Verbindungsregeln: Wie weiß der Generator, welche Prefabs aneinanderpassen? Tags an den Ausgängen? - Steuerung durch Daten: Welche Prefabs auf welcher Etage, mit welcher Gewichtung? Pflichträume (Eingang, Treppe, Boss) versus optionale? - Garantien: Wie wird sichergestellt, dass die Treppe erreichbar ist? - Wie greift ein Mod ein — nur neue Prefabs hinzufügen, oder auch Generator-Parameter ändern? Ergebnis: Prefab-Schema mit Beispiel plus die Regeln, nach denen der Generator zusammensetzt. ## Vorgabe aus Ticket 07 `Game` besitzt genau **einen** `Floor`, dessen Innenbau — Grid, Räume, Bodenloot, Ausgänge — dieses Ticket entscheidet. Besuchte Etagen werden verworfen, es gibt kein Backtracking: Der Generator muss also keine begehbaren Rückwege garantieren, aber **ein Händlerraum muss auf jeder Etage auftauchen können**, sonst ist Handel nach dem Abstieg unerreichbar. Bodenloot ist `ItemInstance`, also `{ def, affixes }`, nicht bloß eine ID. ## Vorgabe aus Ticket 03 Prefabs sind eine **weitere Registry über dieselbe Ladepipeline**, kein eigener Lader — das beantwortet die letzte Frage oben halb: Hinzufügen ist der Normalfall, Ersetzen geht über `overrides`. Offen bleibt, ob **Generator-Parameter** (Etagen-Tabellen, Gewichtungen, Pflichträume) ebenfalls Registry-Einträge sind — dann mit derselben Maschine moddbar — oder ein Sonderfall. ## Answer Begriffe im [Glossar](../../../CONTEXT.md). ### Etagenaufbau: freie Platzierung mit Korridoren Prefabs werden nacheinander an zufälliger Position gesetzt, Überlappung (plus ein Feld Rand) verworfen, nach N Fehlversuchen aufgegeben — der klassische Rogue-Ansatz. Verbunden wird **Raum `i` mit Raum `i−1`** in Platzierungsreihenfolge, danach ein paar zufällige Zusatzkorridore für Schleifen. Korridore als L-Form, erst horizontal, dann vertikal. Die Kette statt eines minimalen Spannbaums, weil sie **Erreichbarkeit strukturell garantiert**: Hängt jeder Raum am vorigen, ist jeder Raum von jedem erreichbar — ohne Flutfüllung und ohne Reparaturschleife. Das beantwortet die Garantiefrage aus dem Ticketkörper nebenbei mit. Prim wären zwanzig Zeilen für dasselbe Ergebnis mit hübscheren Korridorlängen. **Rotation und Spiegelung sind erlaubt** — acht Varianten je Prefab, und das Drehen eines ASCII-Blocks ist eine Transposition. Achtmal so viel Inhalt aus derselben Handarbeit. Kein Opt-out-Feld: 1.0 hat kein Prefab, dessen Ausrichtung etwas bedeutet. Taucht eines auf, ist `rotate = false` eine Zeile — vorher wäre es ein Schalter, den niemand stellt. ### Ein Prefab ```toml [[prefabs]] id = "core:kreuzung" tags = ["core:normal"] layout = """ ####+#### #.......# #..e...e# +.......+ #..l....# #.......# ####+#### """ ``` | Zeichen | Bedeutung | | Zeichen | Bedeutung | |---|---|---|---|---| | `#` | Wand | | `l` | Beute-Platz | | `.` | Boden | | `>` | Treppe | | `+` | Tür | | `<` | Eingang | | `e` | Gegner-Platz | | `n` | NPC-Platz | Kein eigener Händler-Marker: **Ein Händler ist ein NPC.** Ein zweites Zeichen für denselben Platz wäre dieselbe Sorte Vermischung, die Ticket 02 beim `ItemType` abgeräumt hat. Marker stehen **im Block**, nicht als Koordinatenliste daneben. Der ganze Sinn von ASCII ist, dass ein Modder den Raum *sieht*; Koordinaten daneben zu pflegen wäre die schlechtere Hälfte von beidem. **Türen sind explizit.** Ohne sie stanzt der Generator Löcher in eine von Hand gezeichnete Wand — wer Prefabs baut, will bestimmen, wo man hineinkommt, sonst hätte man auch würfeln können. Korridore dürfen nur an `+` andocken. **Befund aus M2: „nur an Türen andocken" ist teurer als es klingt.** Der Test, der Wandfelder zählt, schlug beim ersten Versuch mit **2340 durchgegrabenen Feldern** an. Zwei Ursachen: 1. Der Korridor startete *auf* der Tür — und weil eine Tür auf der Raumwand liegt, grub der waagerechte Abschnitt die ganze Wandreihe auf. Erst einen Schritt **nach außen** treten: 2340 → 352. 2. Zwei L-Varianten reichen nicht. Bei neun Räumen findet sich oft keine, die alle Rechtecke umfährt — sie senken die Trefferquote, garantieren aber nichts. Was hält: eine **Wegsuche über den freien Raum**. Raumfelder liegen nicht im Suchraum, also kann sie eine Wand gar nicht berühren. Die L-Form bleibt davor, weil sie Korridore erzeugt, die wie Korridore aussehen statt wie Treppenmuster; die Wegsuche greift nur, wenn keine L-Variante frei ist. Findet auch sie keinen Weg, wird **nichts** gegraben — dann schlägt der Erreichbarkeitstest an, und das ist die richtige Fehlerrichtung: laut statt still. Ohne den zählenden Test wäre nichts davon aufgefallen. Die Etagen waren begehbar, zusammenhängend und plausibel — der Fehler war rein optisch, und optische Fehler findet niemand durch Nachdenken. **Ein `e` sagt „hier steht ein Gegner", nicht *welcher*.** Was einzieht, entscheidet die Etage. Sonst wäre jedes Prefab an eine Schwierigkeitsstufe gebunden und derselbe hübsche Raum ließe sich nicht auf Etage 1 *und* auf Etage 5 verwenden — genau die Wiederverwendung, für die man Prefabs baut. Bei `n` ist es **umgekehrt**: Den NPC nennt das Prefab selbst (`npcs = ["core:blacksmith"]`), nicht die Etage. NPCs skalieren nicht mit der Tiefe — ein Schmied auf Etage 5 ist derselbe Schmied wie auf Etage 1, und ein `core:merchant`-Raum, dessen Händler ausgewürfelt würde, wäre kein Händlerraum. Das Feld selbst gehört dem NPC-Ticket; hier steht nur, auf welcher Seite die Entscheidung liegt. ### Die Etage ```toml [[floors]] id = "core:floor_3" number = 3 size = [80, 40] # Felder rooms = 9 # Zielzahl inklusive Pflichtraeume required = ["core:entrance", "core:stairs", "core:merchant"] # Tags prefabs = [ { tag = "core:normal", weight = 10 }, { tag = "core:treasure", weight = 2 } ] creatures = [ { id = "core:skeleton", weight = 5 }, { id = "core:ghoul", weight = 2 } ] loot = "core:floor_3_table" ``` Ablauf: **Pflichträume zuerst** auf die leere Fläche, danach gewichtet auffüllen bis `rooms` erreicht oder die Versuche erschöpft sind. Ausgewählt wird durchgehend über **Tags**, auch bei den Pflichträumen. IDs würden bedeuten, dass ein Mod keinen eigenen Eingangsraum beisteuern kann, ohne die ganze Etagen-Tabelle zu überschreiben. Dass Pflichträume zuerst kommen, *ist* die Garantie — auf einer leeren Fläche passen sie praktisch immer. Optionale Räume, die nicht mehr passen, entfallen einfach; das ist der Normalfall, kein Fehler. Passt die Flächensumme der Pflicht-Tags **grundsätzlich** nicht in `size`, ist das ein Datenfehler und beim Laden erkennbar: Warnzeile über die Maschinerie aus 04. Zur Laufzeit dann kein Wiederholungszähler und kein Rückfallpfad — die Prüfung sitzt dort, wo der Fehler gemacht wurde. `core:merchant` steht unter `required` und nicht unter `prefabs`: Ticket 07 hat Backtracking ausgeschlossen, der Händler muss also garantiert auftauchen statt gewichtet. Damit beantwortet sich auch die Frage aus der 03-Vorgabe: **Generator-Parameter sind Daten.** `prefabs` und `floors` sind zwei gewöhnliche Registries — ein Mod ergänzt Etage 6 oder ersetzt Etage 3 über `overrides`, ohne dass hier etwas erfunden wird. Die Gesamtzahl steigt damit auf **zehn**. ### Kampf beginnt beim Betreten eines Raums Die Bestellung aus 14, wer `Game.enemies` füllt: **Gegner gehören zum Raum, nicht zur Etage.** Ein `room_entered` in einen Raum mit lebenden Gegnern verschiebt sie nach `Game.enemies` und startet den Kampf; nach dem Sieg gilt der Raum als geräumt und ein erneutes Betreten startet nichts mehr. Der Grund ist, was dadurch **nicht** gebaut werden muss: Frei umherlaufende Gegner bräuchten Pfadsuche, Sichtlinien, Verfolgung und ein Bewegungsbudget pro Zug — ein eigenes System, das niemand bestellt hat und das die Charter mit „Kampf im Menü" auch nicht nahelegt. Die `e`-Plätze aus dem Prefab sind schlicht die Gegnerliste des Raums. ### `Floor` — der Innenbau aus Ticket 07 ```rust struct Floor { number: u8, tiles: Vec, width: u16, height: u16, rooms: Vec, player_room: Option, } struct Room { prefab: PrefabId, // fuer room_entered und Trigger rect: Rect, enemies: Vec, loot_table: LootTableId, // Nachtrag 16: Prefab-loot oder Etagentabelle loot: Vec, // Haufen, erst beim Aufheben aufgeloest cleared: bool, } enum Tile { Wall, Floor, Door, Stairs, Entrance } ``` Korridore gehören zu **keinem** Raum — sie sind schlicht `Tile::Floor` außerhalb aller `rect`. Dadurch feuert `room_entered` sauber beim Übergang Korridor → Raum, ohne Sonderfall, und `player_room: None` ist ein echter Zustand statt eines Fehlers. Gegner werden bei Kampfbeginn **verschoben, nicht kopiert** — sonst gäbe es zwei Wahrheiten über denselben Gegner. Ladeprüfung über die 04-Maschinerie: **`layout`-Zeilen müssen gleich lang sein**, sonst ist der Block kein Rechteck und die Platzierung rechnet mit Müll. Ein Zweizeiler, der eine ganze Klasse von Modder-Frust abfängt. ### Nachtrag aus Ticket 16 **Bodenbeute wird erst beim Aufheben gezogen**, deshalb hält `Room.loot` nur noch Positionen und der Raum *eine* `loot_table` — alle Haufen eines Raums ziehen ohnehin aus derselben Tabelle (dem Prefab-`loot` oder, wenn es fehlt, der Etagentabelle). Die *Zahl* der Haufen steht weiterhin bei der Erzeugung fest, es sind die `l`-Zeichen; ihr Inhalt nicht. Der Save wird dadurch kleiner. Möglich ist das nur, weil **alle Haufen dasselbe generische Symbol zeigen**: Die Anzeige verrät damit nichts, was noch nicht gewürfelt ist. Der Gewinn ist, dass ein Glücksring auch auf Beute wirkt, die auf dieser Etage schon liegt. Dazu bekommt ein Prefab ein **optionales `loot`-Feld**, das die Etagentabelle für seine `l`-Plätze ersetzt — sonst wäre ein `core:treasure`-Raum nur ein Raum mit mehr `l`-Zeichen. Es nennt eine Tabellen-ID, keine Gegenstände, und bleibt damit über alle Tiefen verwendbar. ### Siegbedingung — bisher nirgends definiert Kein Ticket hatte je festgelegt, was beim Abstieg von der letzten Etage passiert. **`Descend` auf der letzten definierten Etage gewinnt den Lauf.** Es gibt keine Zahl „5" im Code: Die Engine sucht `[[floors]]` mit `number = aktuell + 1` und findet sie keine, ist Schluss. Ein Mod, der Etage 6 ergänzt, verlängert das Spiel ohne eine Zeile Engine-Code — dieselbe Bewegung wie bei `core:attack` in Ticket 11: Was wie Engine-Logik aussieht, ist Inhalt. ### Zwei Nachträge an geschlossene Tickets | Ticket | Nachtrag | |---|---| | **08** | `room_entered { prefab_id }` als **zwölftes** Event. `floor_entered` deckt nur die Etage ab; „die Tür fällt hinter dir zu", ein Hinterhalt oder eine Quest „finde die Bibliothek" brauchen den einzelnen Raum. Neue Maschinerie kostet es nicht — Prefabs sind gewöhnlicher Registry-Inhalt und dürfen Trigger tragen wie alles andere | | **14** | `GameOver { outcome: Died \| Won }` statt nur `GameOver`. Der Bildschirm zeigt in beiden Fällen dasselbe — Etage, Zug, Gold, Verlauf —, nur die Überschrift unterscheidet sich. Zwei Screens für zwei Texte wären Verdopplung | ### Auflagen an andere Tickets - **09 Save-Schema**: Layout oder Seed? Ticket 14 hat einen seedbaren Generator geliefert, Regenerieren *wäre* also möglich — aber der Raumzustand (`cleared`, gefallene Gegner, genommene Beute) lässt sich nicht regenerieren und muss ohnehin mit. 09 entscheidet, ob sich der Seed dann noch lohnt. - **16 Loot-Tabellen**: `loot` an der Etage verweist auf eine Tabelle, die `l`-Plätze ziehen daraus. - **Boss-Design**: Ein Boss ist derzeit ein Gegner in einem Prefab mit `core:boss`-Tag. Phasen fehlen weiterhin.