12 KiB
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.
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
[[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:
- 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.
- 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
[[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
struct Floor {
number: u8,
tiles: Vec<Tile>, width: u16, height: u16,
rooms: Vec<Room>,
player_room: Option<usize>,
}
struct Room {
prefab: PrefabId, // fuer room_entered und Trigger
rect: Rect,
enemies: Vec<Creature>,
loot_table: LootTableId, // Nachtrag 16: Prefab-loot oder Etagentabelle
loot: Vec<Pos>, // 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:
lootan der Etage verweist auf eine Tabelle, diel-Plätze ziehen daraus. - Boss-Design: Ein Boss ist derzeit ein Gegner in einem Prefab mit
core:boss-Tag. Phasen fehlen weiterhin.