Files
rustkeep/.scratch/rustkeep-1.0/issues/10-raum-prefab-format.md

9.8 KiB
Raw Blame History

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.

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: Vec<(Pos, ItemInstance)>,
    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.

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.