205 lines
9.8 KiB
Markdown
205 lines
9.8 KiB
Markdown
# 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.
|
||
|
||
**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<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.
|