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

238 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<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**: `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.